diff --git a/components/application/index.css b/components/application/index.css index 106356140..ea2fbfdd4 100644 --- a/components/application/index.css +++ b/components/application/index.css @@ -189,13 +189,22 @@ #applicationfx .installwrapper { display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; align-items: center; text-align: center; max-width: 600px; - height: 80vh; + min-height: 100vh; margin: auto; - padding: 0; + padding: calc(55px + var(--app-margin-top)) 0 40px 0; + overflow-y: auto; +} + +#applicationfx .installContentWrapper { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding-top: 40px; } #applicationfx .installwrapper .appicon { width: 96px; @@ -369,3 +378,825 @@ backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } + +/* Ratings Preview in Install Page */ +#applicationfx .ratingsPreviewSection { + margin-top: 40px; + padding: 0 20px; + max-width: 600px; + width: 100%; +} + +#applicationfx .ratingsPreviewHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +#applicationfx .ratingsPreviewHeader h3 { + font-size: 1.5em; + font-weight: 700; + color: rgb(var(--text-color)); + margin: 0; +} + +#applicationfx .viewAllReviews { + background: none; + border: none; + color: rgb(var(--color-bg-ac)); + font-size: 1em; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; + padding: 8px 12px; + border-radius: 8px; + transition: background 0.2s; +} + +#applicationfx .viewAllReviews:hover { + background: rgba(var(--color-bg-ac), 0.1); +} + +#applicationfx .ratingsPreview { + display: flex; + flex-direction: column; + gap: 20px; +} + +#applicationfx .ratingPreviewCard { + background: rgb(var(--neutral-grad-1)); + border-radius: 16px; + padding: 20px; + border: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .overallRatingPreview { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 8px; +} + +#applicationfx .ratingScorePreview { + display: flex; + align-items: baseline; + gap: 4px; +} + +#applicationfx .scoreNumberLarge { + font-size: 3em; + font-weight: 700; + color: rgb(var(--text-color)); +} + +#applicationfx .scoreOutOfSmall { + font-size: 1.2em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .starsPreview { + display: flex; + gap: 4px; + color: #FFB800; + font-size: 1.2em; +} + +#applicationfx .ratingsCountPreview { + font-size: 0.9em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .recentReviewsPreview { + display: flex; + flex-direction: column; + gap: 16px; +} + +#applicationfx .reviewPreviewItem { + background: rgb(var(--neutral-grad-1)); + border-radius: 12px; + padding: 16px; + border: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .reviewPreviewHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +#applicationfx .reviewPreviewStars { + display: flex; + gap: 2px; + color: #FFB800; + font-size: 0.9em; +} + +#applicationfx .reviewPreviewDate { + font-size: 0.85em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.6; +} + +#applicationfx .reviewPreviewUser { + font-weight: 600; + color: rgb(var(--text-color)); + margin-bottom: 4px; +} + +#applicationfx .reviewPreviewTitle { + font-weight: 600; + color: rgb(var(--text-color)); + margin-bottom: 4px; + font-size: 0.95em; +} + +#applicationfx .reviewPreviewText { + font-size: 0.9em; + color: rgb(var(--text-on-main-bg-color)); + line-height: 1.4; +} + +#applicationfx .ratingsPreviewEmpty { + padding: 40px 20px; +} + +#applicationfx .emptyRatingCard { + background: rgb(var(--neutral-grad-1)); + border-radius: 16px; + padding: 40px 20px; + text-align: center; + border: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .emptyStars { + display: flex; + justify-content: center; + gap: 6px; + color: rgb(var(--neutral-grad-0)); + font-size: 2em; + margin-bottom: 20px; +} + +#applicationfx .emptyRatingCard p { + margin: 8px 0; + color: rgb(var(--text-color)); + font-size: 1.1em; + font-weight: 600; +} + +#applicationfx .emptySubtext { + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; + font-size: 0.9em !important; + font-weight: 400 !important; +} + +/* Reviews Page Styles */ +#applicationfx .reviewsWrapper { + padding: calc(55px + var(--app-margin-top)) 0 20px 0; + max-width: 900px; + margin: 0 auto; + overflow-y: auto; + height: 100vh; +} + +#applicationfx .reviewsAppHeader { + display: flex; + align-items: center; + gap: 12px; + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .appIconSmall { + width: 60px; + height: 60px; + border-radius: 12px; + background: rgba(var(--neutral-grad-1), 0.3); +} + +#applicationfx .appInfo { + flex: 1; +} + +#applicationfx .appInfo .appName { + font-size: 1.3em; + font-weight: 700; + color: rgb(var(--text-color)); +} + +#applicationfx .appInfo .appDeveloper { + font-size: 0.9em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .overallRating { + display: flex; + align-items: center; + gap: 24px; + padding: 30px 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .ratingScore { + display: flex; + flex-direction: column; + align-items: center; + min-width: 100px; +} + +#applicationfx .scoreNumber { + font-size: 3.5em; + font-weight: 700; + color: rgb(var(--text-color)); + line-height: 1; +} + +#applicationfx .scoreOutOf { + font-size: 1em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .ratingDetails { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +#applicationfx .starsDisplay { + display: flex; + gap: 4px; + color: #FFB800; + font-size: 1.5em; +} + +#applicationfx .ratingCount { + font-size: 0.95em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .ratingDistribution { + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .distributionRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +#applicationfx .distributionStars { + display: flex; + gap: 2px; + color: #FFB800; + font-size: 0.8em; + min-width: 60px; +} + +#applicationfx .distributionBar { + flex: 1; + height: 8px; + background: rgb(var(--neutral-grad-0)); + border-radius: 4px; + overflow: hidden; +} + +#applicationfx .distributionBarFill { + height: 100%; + background: #FFB800; + transition: width 0.3s ease; +} + +#applicationfx .distributionPercent { + min-width: 40px; + text-align: right; + font-size: 0.9em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .writeReviewSection { + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + display: flex; + gap: 12px; +} + +#applicationfx .rateAppButton, +#applicationfx .writeReviewButton { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +#applicationfx .userRatingDisplay { + flex: 1; + background: rgba(var(--color-bg-ac), 0.1); + border: 1px solid rgba(var(--color-bg-ac), 0.3); + border-radius: 12px; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +#applicationfx .userRatingInfo { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +#applicationfx .userRatingLabel { + font-weight: 600; + color: rgb(var(--text-color)); + font-size: 0.95em; +} + +#applicationfx .userRatingStars { + display: flex; + gap: 2px; + color: #FFB800; + font-size: 1em; +} + +#applicationfx .userRatingValue { + font-weight: 600; + color: rgb(var(--color-bg-ac)); + font-size: 0.95em; +} + +#applicationfx .userRatingNote { + font-size: 0.85em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .reviewsList { + padding: 20px; +} + +#applicationfx .noReviews { + text-align: center; + padding: 40px 20px; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .noReviews p { + font-size: 1em; + margin: 0; +} + +#applicationfx .reviewsHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +#applicationfx .reviewsHeader h3 { + font-size: 1.3em; + font-weight: 700; + color: rgb(var(--text-color)); + margin: 0; +} + +#applicationfx .sortSelect { + padding: 8px 12px; + border-radius: 8px; + border: 1px solid rgb(var(--neutral-grad-0)); + background: rgb(var(--neutral-grad-1)); + color: rgb(var(--text-color)); + font-size: 0.9em; + cursor: pointer; +} + +#applicationfx .reviewItem { + padding: 20px; + margin-bottom: 16px; + background: rgb(var(--neutral-grad-1)); + border: 1px solid rgb(var(--neutral-grad-0)); + border-radius: 12px; +} + +#applicationfx .reviewHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +#applicationfx .reviewUserInfo { + display: flex; + gap: 12px; + align-items: center; +} + +#applicationfx .reviewUserAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: rgb(var(--neutral-grad-0)); + display: flex; + align-items: center; + justify-content: center; + color: rgb(var(--text-on-main-bg-color)); +} + +#applicationfx .reviewUserName { + font-weight: 600; + color: rgb(var(--text-color)); + font-size: 0.95em; +} + +#applicationfx .reviewDate { + font-size: 0.85em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.6; +} + +#applicationfx .reviewRating { + display: flex; + gap: 2px; + color: #FFB800; + font-size: 0.9em; +} + +#applicationfx .reviewTitle { + font-weight: 600; + color: rgb(var(--text-color)); + margin-bottom: 8px; + font-size: 1em; +} + +#applicationfx .reviewText { + color: rgb(var(--text-on-main-bg-color)); + line-height: 1.6; + font-size: 0.95em; + margin-bottom: 12px; +} + +#applicationfx .developerResponse { + background: rgba(var(--color-bg-ac), 0.05); + border-left: 3px solid rgb(var(--color-bg-ac)); + padding: 12px; + margin-top: 12px; + border-radius: 8px; +} + +#applicationfx .developerResponseHeader { + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + color: rgb(var(--color-bg-ac)); + margin-bottom: 8px; + font-size: 0.9em; +} + +#applicationfx .developerResponseText { + color: rgb(var(--text-on-main-bg-color)); + font-size: 0.9em; + line-height: 1.5; + margin-bottom: 6px; +} + +#applicationfx .developerResponseDate { + font-size: 0.8em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.6; +} + +#applicationfx .reviewActions { + display: flex; + gap: 8px; + align-items: center; + opacity: 0; + transition: opacity 0.2s ease; +} + +#applicationfx .reviewItem:hover .reviewActions { + opacity: 1; +} + +#applicationfx .reviewActions .deleteReviewButton { + background: none; + border: none; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.5; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + font-size: 0.9em; + transition: all 0.2s ease; +} + +#applicationfx .reviewActions .deleteReviewButton:hover { + opacity: 1; + background: rgba(var(--color-bg-ac-error), 0.1); + color: rgb(var(--color-bg-ac-error)); +} + +#applicationfx .reviewActionButton { + background: none; + border: none; + color: rgb(var(--text-on-main-bg-color)); + font-size: 0.85em; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 8px; + transition: background 0.2s; +} + +#applicationfx .reviewActionButton:hover { + background: rgb(var(--neutral-grad-0)); +} + +#applicationfx .loadMoreSection { + display: flex; + justify-content: center; + padding: 20px; +} + +#applicationfx .loadMoreButton { + min-width: 200px; +} + +/* Review Form Styles */ +#applicationfx .reviewFormWrapper { + padding: calc(55px + var(--app-margin-top)) 20px 40px 20px; + max-width: 700px; + margin: 0 auto; + overflow-y: auto; + height: 100vh; +} + +#applicationfx .reviewFormAppHeader { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + margin-bottom: 30px; +} + +#applicationfx .reviewFormAppHeader .appIconSmall { + width: 80px; + height: 80px; +} + +#applicationfx .reviewFormAppHeader .appName { + font-size: 1.4em; + font-weight: 700; + color: rgb(var(--text-color)); + text-align: center; +} + +#applicationfx .reviewFormRating { + text-align: center; + padding: 30px 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .reviewFormRating h3 { + font-size: 1.3em; + font-weight: 700; + color: rgb(var(--text-color)); + margin: 0 0 20px 0; +} + +#applicationfx .starRatingInput { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 12px; +} + +#applicationfx .starRatingInput i { + font-size: 2.5em; + color: #FFB800; + cursor: pointer; + transition: transform 0.2s; +} + +#applicationfx .starRatingInput i:hover { + transform: scale(1.2); +} + +#applicationfx .ratingLabel { + font-size: 0.95em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .reviewFormFields { + padding: 30px 20px; +} + +#applicationfx .formGroup { + margin-bottom: 24px; +} + +#applicationfx .formGroup label { + display: block; + font-weight: 600; + color: rgb(var(--text-color)); + margin-bottom: 8px; + font-size: 0.95em; +} + +#applicationfx .formInput, +#applicationfx .formTextarea { + width: 100%; + padding: 12px; + border: 1px solid rgb(var(--neutral-grad-0)); + border-radius: 8px; + background: rgb(var(--neutral-grad-1)); + color: rgb(var(--text-color)); + font-size: 0.95em; + font-family: inherit; + resize: vertical; +} + +#applicationfx .formInput:focus, +#applicationfx .formTextarea:focus { + outline: none; + border-color: rgb(var(--color-bg-ac)); +} + +#applicationfx .characterCount { + text-align: right; + font-size: 0.85em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.6; + margin-top: 4px; +} + +#applicationfx .reviewGuidelines { + background: rgba(var(--color-bg-ac), 0.05); + border-radius: 8px; + padding: 16px; + margin-bottom: 24px; +} + +#applicationfx .guidelinesTitle { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: rgb(var(--text-color)); + margin-bottom: 12px; +} + +#applicationfx .reviewGuidelines ul { + margin: 0; + padding-left: 20px; + list-style: disc; +} + +#applicationfx .reviewGuidelines li { + color: rgb(var(--text-on-main-bg-color)); + font-size: 0.9em; + line-height: 1.6; + margin-bottom: 6px; +} + +#applicationfx .reviewFormActions { + display: flex; + gap: 12px; + justify-content: center; +} + +#applicationfx .reviewFormActions .button { + min-width: 140px; +} + +#applicationfx .cancelButton { + background: rgb(var(--neutral-grad-1)); + color: rgb(var(--text-color)); + border: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .cancelButton:hover { + background: rgb(var(--neutral-grad-0)); +} + +#applicationfx .submitReviewButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Rating Form Styles */ +#applicationfx .ratingFormWrapper { + padding: calc(55px + var(--app-margin-top)) 20px 40px 20px; + max-width: 500px; + margin: 0 auto; + overflow-y: auto; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; +} + +#applicationfx .ratingFormAppHeader { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 40px 20px; + text-align: center; +} + +#applicationfx .ratingFormAppHeader .appIconLarge { + width: 100px; + height: 100px; + border-radius: 20px; + background: rgba(var(--neutral-grad-1), 0.3); +} + +#applicationfx .ratingFormAppHeader .appName { + font-size: 1.6em; + font-weight: 700; + color: rgb(var(--text-color)); +} + +#applicationfx .ratingFormAppHeader .ratingPrompt { + font-size: 1.1em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.8; +} + +#applicationfx .ratingFormStars { + text-align: center; + padding: 40px 20px; +} + +#applicationfx .ratingFormStars .starRatingInput { + display: flex; + justify-content: center; + gap: 12px; + margin-bottom: 20px; +} + +#applicationfx .ratingFormStars .starRatingInput i { + font-size: 3em; + color: #FFB800; + cursor: pointer; + transition: transform 0.2s; +} + +#applicationfx .ratingFormStars .starRatingInput i:hover { + transform: scale(1.15); +} + +#applicationfx .ratingFormStars .ratingLabel { + font-size: 1em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.7; +} + +#applicationfx .ratingFormActions { + display: flex; + gap: 12px; + justify-content: center; + padding: 20px; +} + +#applicationfx .ratingFormActions .button { + min-width: 140px; +} + +#applicationfx .cancelRatingButton { + background: rgb(var(--neutral-grad-1)); + color: rgb(var(--text-color)); + border: 1px solid rgb(var(--neutral-grad-0)); +} + +#applicationfx .cancelRatingButton:hover { + background: rgb(var(--neutral-grad-0)); +} + +#applicationfx .submitRatingButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/components/application/index.js b/components/application/index.js index b172cc076..2ece27f97 100644 --- a/components/application/index.js +++ b/components/application/index.js @@ -8,9 +8,189 @@ var application = (function(){ var primary = deep(p, 'history'); - var el, ed, application, appdata, curpath, userAddress, isUserAuthor, grantedPermissions; + var el, ed, application, appdata, curpath, userAddress, isUserAuthor, grantedPermissions, scores, userRating, comments, hasReviewsSupport; + + var calculateRatingStats = function(scores) { + if (!scores || !scores.length) { + return { + averageRating: 0, + totalRatings: 0, + distribution: [ + { stars: 5, count: 0, percentage: 0 }, + { stars: 4, count: 0, percentage: 0 }, + { stars: 3, count: 0, percentage: 0 }, + { stars: 2, count: 0, percentage: 0 }, + { stars: 1, count: 0, percentage: 0 } + ] + } + } + + var total = scores.length + var sum = 0 + var distribution = [0, 0, 0, 0, 0] + + scores.forEach(function(score) { + var value = parseInt(score.value) || 0 + sum += value + if (value >= 1 && value <= 5) { + distribution[value - 1]++ + } + }) + + var average = total > 0 ? (sum / total).toFixed(1) : 0 + + var createDistributionItem = function(stars, index) { + return { + stars: stars, + count: distribution[index], + percentage: total > 0 ? Math.round((distribution[index] / total) * 100) : 0 + } + } + + return { + averageRating: parseFloat(average), + totalRatings: total, + distribution: [ + createDistributionItem(5, 4), + createDistributionItem(4, 3), + createDistributionItem(3, 2), + createDistributionItem(2, 1), + createDistributionItem(1, 0) + ] + } + } + + var findUserRating = function(scores, userAddress) { + if (!scores || !userAddress) return null + var userScore = scores.find(function(score) { + return score.address === userAddress + }) + return userScore ? parseInt(userScore.value) : null + } + + var mapCommentsToReviews = function(comments) { + if (!comments || !comments.length) return [] + + return comments.filter(comment => !comment.deleted).map(function(comment) { + console.log(comment, 'comment') + var userInfo = self.psdk.userInfo.get(comment.address) || {} + console.log(userInfo, 'userInfo') + return { + userName: userInfo.name || 'Anonymous', + text: comment.message || '', + userAvatar: userInfo.image, + date: comment.time ? new Date(comment.time).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' }) : '', + id: comment.id, + address: comment.address + } + }) + } + + var loadUserInfoForComments = function(comments, callback) { + if (!comments || comments.length === 0) { + if (callback) callback() + return + } + + var addresses = comments.map(function(c) { return c.address }).filter(Boolean) + if (addresses.length === 0) { + if (callback) callback() + return + } + + self.psdk.userInfo.load(addresses).then(callback).catch(function(err){ + console.error('Error loading user info:', err) + if (callback) callback() + }) + } var actions = { + deleteComment : function(comment, clbk){ + var ct = comment.delete() + + self.app.platform.sdk.comments.delete(ct, function(err, alias){ + + if(!err){ + if (clbk) + clbk(null, alias) + } + + else + { + self.app.platform.errorHandler(err, true) + + if (clbk) + clbk(err, null) + } + + }) + }, + + loadscores : function(callback){ + if (!application || !application.hash) { + if (callback) callback() + return + } + + self.app.platform.sdk.postscores.get(application.hash, function(){ + scores = self.psdk.postScores.get(application.hash) || [] + userRating = findUserRating(scores, userAddress) + if (callback) callback() + }) + }, + + loadcomments : function(callback){ + if (!application || !application.hash) { + if (callback) callback([]) + return + } + var nodes = ['94.156.128.149:38081'] + self.app.platform.sdk.comments.getclear(application.hash, "", function (loadedComments, error) { + if (error) { + console.error('Error loading comments:', error) + comments = [] + if (callback) callback(comments) + } else { + comments = loadedComments || [] + loadUserInfoForComments(comments, function () { + if (callback) callback(comments) + }) + } + }, { + rpc: { + fnode: nodes[rand(0, nodes.length - 1)] + } + }) + }, + + openreviews : function(){ + globalpreloader(true, true) + if (!hasReviewsSupport) return + + actions.loadscores(function(){ + actions.loadcomments(function(){ + globalpreloader(false) + renders.reviews() + }) + }) + }, + + openreviewform : function(){ + if (!application.installed) { + sitemessage(self.app.localization.e('mustInstallToComment')) + return + } + renders.reviewform() + }, + + openratingform : function(){ + if (!application.installed) { + sitemessage(self.app.localization.e('mustInstallToRate')) + return + } + renders.ratingform() + }, + install : function(){ @@ -241,6 +421,12 @@ var application = (function(){ close() }) + el.find('.reviews').on('click', function(){ + actions.openreviews() + + close() + }) + el.find('.close').on('click', function(){ actions.gotohome() @@ -250,15 +436,129 @@ var application = (function(){ }) }, d) - - + + }, install : function(clbk){ + actions.loadscores(function(){ + actions.loadcomments(function(){ + var ratingStats = calculateRatingStats(scores) + var reviewsList = mapCommentsToReviews(comments) + + var reviewsData = Object.assign({}, ratingStats) + if (reviewsList.length > 0) { + reviewsData.reviews = reviewsList + } + + self.shell({ + name : 'install', + el : el.c, + data : { + application, + ratingsData: reviewsData, + hasReviewsSupport: hasReviewsSupport + }, + }, function(p){ + p.el.find('.installButton button').on('click', function(){ + actions.install() + }) + + p.el.find('.back').on('click', function(){ + actions.gotohome() + }) + + p.el.find('.viewAllReviews').on('click', function(){ + actions.openreviews() + }) + + events.pageevents(p, true) + + if (clbk) + clbk(); + }) + }) + }) + }, + + reviews : function(clbk){ + var ratingStats = calculateRatingStats(scores) + var reviewsList = mapCommentsToReviews(comments) + + var reviewsData = Object.assign({}, ratingStats) + if (reviewsList.length > 0) { + reviewsData.reviews = reviewsList + } + const canWriteReview = application.installed && !!userAddress && appdata.address !== userAddress + self.shell({ + name : 'reviews', + el : el.c, + data : { + application, + ratingsData: reviewsData, + canWriteReview, + userRating: userRating, + hasUserRated: userRating !== null, + userAddress: userAddress + }, + }, function(p){ + p.el.find('.back').on('click', function(){ + if(application.installed){ + renders.frameremote() + } else { + renders.install() + } + }) + + p.el.find('.rateAppButton').on('click', function(){ + actions.openratingform() + }) + + p.el.find('.writeReviewButton').on('click', function(){ + actions.openreviewform() + }) + + p.el.find('.deleteReviewButton').on('click', function(){ + var commentId = $(this).data('comment-id') + var comment = comments.find(function(c){ return c.id === commentId }) + + if (!comment) return + + new dialog({ + html : self.app.localization.e('e13032'), + success : function(){ + + actions.deleteComment(comment, function(err){ + + if(!err) + { + sitemessage('Comment deleted') + actions.loadcomments(function(){ + renders.reviews() + }) + } + + }) + + }, + btn1text : self.app.localization.e('e13034'), + btn2text : self.app.localization.e('e13035'), + class : 'zindex', + }) + }) + + events.pageevents(p, true) + + if (clbk) + clbk(); + }) + }, + + reviewform : function(clbk){ self.shell({ - name : 'install', + name : 'reviewform', el : el.c, data : { application @@ -266,17 +566,137 @@ var application = (function(){ }, function(p){ + p.el.find('.back').on('click', function(){ + renders.reviews() + }) + + p.el.find('#reviewText').on('input', function(){ + var length = $(this).val().length + p.el.find('#reviewText').closest('.formGroup').find('.currentCount').text(length) + validateForm() + }) + + var validateForm = function(){ + var reviewText = p.el.find('#reviewText').val().trim() + var isValid = reviewText.length >= 10 + + p.el.find('.submitReviewButton').prop('disabled', !isValid) + } - p.el.find('.installButton button').on('click', function(){ - actions.install() + p.el.find('.cancelButton').on('click', function(){ + renders.reviews() }) + p.el.find('.submitReviewButton').on('click', function(){ + var btn = $(this) + var reviewText = p.el.find('#reviewText').val().trim() + + if (!reviewText || reviewText.length < 10) { + sitemessage(self.app.localization.e('commentMinLength')) + return + } + + btn.prop('disabled', true) + btn.html('') + + var comment = new Comment(application.hash) + comment.message.set(reviewText) + + self.app.platform.sdk.comments.send(comment, function(error, alias){ + if (error) { + console.error('Error sending comment:', error) + sitemessage(self.app.localization.e('commentSendError')) + btn.prop('disabled', false) + btn.html(self.app.localization.e('submitReview')) + } else { + sitemessage(self.app.localization.e('commentSent')) + renders.reviews() + } + }) + }) + + events.pageevents(p, true) + + if (clbk) + clbk(); + }) + }, + + ratingform : function(clbk){ + + var selectedRating = 0 + + self.shell({ + + name : 'ratingform', + el : el.c, + data : { + application + }, + + }, function(p){ + p.el.find('.back').on('click', function(){ - actions.gotohome() + renders.reviews() + }) + + p.el.find('.starRatingInput i').on('click', function(){ + selectedRating = $(this).data('rating') + + p.el.find('.starRatingInput i').each(function(){ + var starRating = $(this).data('rating') + $(this).removeClass('fas far').addClass(starRating <= selectedRating ? 'fas' : 'far') + }) + + p.el.find('.ratingLabel').text(self.app.localization.e('yourRating') + ': ' + selectedRating + ' ' + self.app.localization.e('outOf5')) + + p.el.find('.submitRatingButton').prop('disabled', false) + }) + + p.el.find('.cancelRatingButton').on('click', function(){ + renders.reviews() + }) + + p.el.find('.submitRatingButton').on('click', function(){ + var btn = $(this) + + btn.prop('disabled', true) + btn.html('') + + self.app.platform.sdk.upvote.checkvalue( + selectedRating, + function () { + const obj = self.psdk.miniapp.get(application.id) + var upvoteShare = obj.upvote(selectedRating, userAddress); + + if (!upvoteShare) { + self.app.platform.errorHandler("4", true); + btn.prop('disabled', false) + btn.html(self.app.localization.e('submitRating')) + return; + } + + self.app.platform.actions + .addActionAndSendIfCan(upvoteShare) + .then(() => { + sitemessage(self.app.localization.e('ratingSent')) + renders.reviews() + }) + .catch((e) => { + self.app.platform.errorHandler(e, true); + btn.prop('disabled', false) + btn.html(self.app.localization.e('submitRating')) + }); + }, + function () { + btn.prop('disabled', false) + btn.html(self.app.localization.e('submitRating')) + } + ); }) events.pageevents(p, true) - + if (clbk) clbk(); }) @@ -372,14 +792,15 @@ var application = (function(){ name : 'frameremote', el : el.c, - + data : { application, iframeAllowAttr, isInDevMode: _scope === tscope, tscope: isUserAuthor && tscope, scope: appdata.scope, - src + src, + hasReviewsSupport: hasReviewsSupport }, }, function(p){ @@ -395,12 +816,16 @@ var application = (function(){ } }) + p.el.find('.reviewsBtn').on('click', function(){ + actions.openreviews() + }) + p.el.find('.forward').on('click', function(){ if (history.length) { - history.forward() + history.forward() } }) - + p.el.find('#domain-switch')?.on('change', function () { const isDevMode = this.checked; renders.frameremote(isDevMode ? tscope : appdata.scope); @@ -536,11 +961,16 @@ var application = (function(){ application = null appdata = null - userAddress = self.app.user.address.value; - self.app.apps.get.application(id).then((f) => { - + userAddress = self.app.user.address.value; + self.app.apps.get.application(id).then(async (f) => { + globalpreloader(true, true) if (f){ - + hasReviewsSupport = !!(self.psdk.miniapp.get(f.application.id)) + if(!hasReviewsSupport) { + const app = await self.sdk.miniapps.getbyid(id) + hasReviewsSupport = !!app + } + application = f.application appdata = f.appdata?.data @@ -568,7 +998,8 @@ var application = (function(){ var data = { ed }; - + + globalpreloader(false) clbk(data); }).catch(e => { @@ -585,10 +1016,9 @@ var application = (function(){ ed }; + globalpreloader(false) clbk(data); - }) - }, destroy : function(){ diff --git a/components/application/index.less b/components/application/index.less index e09147f5d..84ae69db5 100644 --- a/components/application/index.less +++ b/components/application/index.less @@ -380,4 +380,186 @@ -webkit-backdrop-filter: none !important; } } + + // Reviews Page Styles + .reviewsWrapper { + padding: calc(55px + var(--app-margin-top)) 0 20px 0; + max-width: 900px; + margin: 0 auto; + overflow-y: auto; + height: 100vh; + } + + .reviewsAppHeader { + display: flex; + align-items: center; + gap: 12px; + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + } + + .reviewItem { + padding: 20px; + margin-bottom: 16px; + background: rgb(var(--neutral-grad-1)); + border: 1px solid rgb(var(--neutral-grad-0)); + border-radius: 12px; + + &:hover { + .reviewActions { + opacity: 1; + } + } + } + + .reviewHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; + } + + .reviewUserInfo { + display: flex; + gap: 12px; + align-items: center; + } + + .reviewUserAvatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: rgb(var(--neutral-grad-0)); + display: flex; + align-items: center; + justify-content: center; + color: rgb(var(--text-on-main-bg-color)); + } + + .reviewUserName { + font-weight: 600; + color: rgb(var(--text-color)); + font-size: 0.95em; + } + + .reviewDate { + font-size: 0.85em; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.6; + } + + .reviewText { + color: rgb(var(--text-on-main-bg-color)); + line-height: 1.6; + font-size: 0.95em; + margin-bottom: 12px; + } + + .reviewActions { + display: flex; + gap: 8px; + align-items: center; + opacity: 0; + transition: opacity 0.2s ease; + + .deleteReviewButton { + background: none; + border: none; + color: rgb(var(--text-on-main-bg-color)); + opacity: 0.5; + cursor: pointer; + padding: 4px 8px; + border-radius: 6px; + font-size: 0.9em; + transition: all 0.2s ease; + + &:hover { + opacity: 1; + background: rgba(var(--color-bg-ac-error), 0.1); + color: rgb(var(--color-bg-ac-error)); + } + } + } + + .writeReviewSection { + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + display: flex; + gap: 12px; + } + + .rateAppButton, + .writeReviewButton { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + .userRatingDisplay { + flex: 1; + background: rgba(var(--color-bg-ac), 0.1); + border: 1px solid rgba(var(--color-bg-ac), 0.3); + border-radius: 12px; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .overallRating { + display: flex; + align-items: center; + gap: 24px; + padding: 30px 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + } + + .ratingScore { + display: flex; + flex-direction: column; + align-items: center; + min-width: 100px; + } + + .scoreNumber { + font-size: 3.5em; + font-weight: 700; + color: rgb(var(--text-color)); + line-height: 1; + } + + .starsDisplay { + display: flex; + gap: 4px; + color: #FFB800; + font-size: 1.5em; + } + + .ratingDistribution { + padding: 20px; + border-bottom: 1px solid rgb(var(--neutral-grad-0)); + } + + .distributionRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + } + + .distributionBar { + flex: 1; + height: 8px; + background: rgb(var(--neutral-grad-0)); + border-radius: 4px; + overflow: hidden; + } + + .distributionBarFill { + height: 100%; + background: #FFB800; + transition: width 0.3s ease; + } } diff --git a/components/application/templates/frameremote.html b/components/application/templates/frameremote.html index caab7bf7f..1d760a609 100644 --- a/components/application/templates/frameremote.html +++ b/components/application/templates/frameremote.html @@ -76,12 +76,26 @@ <% } %> <% if(app.mobileview || app.electronview) {%> + <% if(hasReviewsSupport) { %> +
+
+ +
+
+ <% } %>
<% } else { %> + <% if(hasReviewsSupport) { %> +
+
+ +
+
+ <% } %>
diff --git a/components/application/templates/install.html b/components/application/templates/install.html index 9478186c8..cbfa28619 100644 --- a/components/application/templates/install.html +++ b/components/application/templates/install.html @@ -16,24 +16,87 @@ <% } %>
- -
-
<%-application.name || "Unknown App"%>
-
<%-application.description || "No description available."%>
-
- <% if (application.tags && application.tags.length) { %> - <% application.tags.forEach(function(tag) { %> - <%- tag %> - <% }) %> +
+ +
+
<%-application.name || "Unknown App"%>
+
<%-application.description || "No description available."%>
+
+ <% if (application.tags && application.tags.length) { %> + <% application.tags.forEach(function(tag) { %> + <%- tag %> + <% }) %> + <% } else { %> + No tags available + <% } %> +
+
+ +
+ +
+ + <% if(hasReviewsSupport) { %> +
+
+

<%- e('ratingsAndReviews') %>

+ +
+ + <% if(ratingsData && ratingsData.totalRatings && ratingsData.totalRatings > 0) { %> +
+
+
+
+
<%- ratingsData.averageRating || 0 %>
+
<%- e('outOf5') %>
+
+
+ <% var avgRating = ratingsData.averageRating || 0; %> + <% for(var i = 1; i <= 5; i++) { %> + + <% } %> +
+
<%- ratingsData.totalRatings || 0 %> <%- e('ratings') %>
+
+
+ + <% if(ratingsData.reviews && ratingsData.reviews.length > 0) { %> +
+ <% ratingsData.reviews.slice(0, 2).forEach(function(review) { %> +
+
+
<%- review.date || '' %>
+
+
<%- review.userName || 'Аноним' %>
+ <% if(review.title) { %> +
<%- review.title %>
+ <% } %> +
<%- review.text ? review.text.substring(0, 100) : '' %>...
+
+ <% }) %> +
+ <% } %> +
<% } else { %> - No tags available +
+
+
+ <% for(var i = 1; i <= 5; i++) { %> + + <% } %> +
+

<%- e('noRatingsYet') %>

+

<%- e('beFirstToRate') %>

+
+
<% } %>
-
- -
- + <% } %>
diff --git a/components/application/templates/metmenu.html b/components/application/templates/metmenu.html index e95e8d833..ebcb30444 100644 --- a/components/application/templates/metmenu.html +++ b/components/application/templates/metmenu.html @@ -10,16 +10,23 @@ class : 'settings' }, + reviews : { + icon : '', + text : e('reviewsAndRatingsMenu'), + class : 'reviews' + }, + close : { icon : '', text : e('application_close'), class : 'close' } - + } if(application && application.manifest){ m.push('settings') + m.push('reviews') } m.push('close') diff --git a/components/application/templates/ratingform.html b/components/application/templates/ratingform.html new file mode 100644 index 000000000..d301b35f8 --- /dev/null +++ b/components/application/templates/ratingform.html @@ -0,0 +1,38 @@ +
+
+
+ +
+
+
+ <%- e('rateApp') %> +
+
+ +
+
+ <%-application.name%> +
<%-application.name || "Unknown App"%>
+
<%- e('howDoYouRate') %>
+
+ +
+
+ + + + + +
+
<%- e('clickStarToRate') %>
+
+ +
+ + +
+
diff --git a/components/application/templates/reviewform.html b/components/application/templates/reviewform.html new file mode 100644 index 000000000..8b3910859 --- /dev/null +++ b/components/application/templates/reviewform.html @@ -0,0 +1,45 @@ +
+
+
+ +
+
+
+ <%- e('writeComment') %> +
+
+ +
+
+ <%-application.name%> +
<%-application.name || "Unknown App"%>
+
+ +
+

<%- e('writeComment') %>

+ +
+ + +
+ 0/500 +
+
+ +
+ + +
+
+
diff --git a/components/application/templates/reviews.html b/components/application/templates/reviews.html new file mode 100644 index 000000000..c903fd84f --- /dev/null +++ b/components/application/templates/reviews.html @@ -0,0 +1,134 @@ +
+
+
+ +
+
+
+ <%-application && application.name ? application.name : ""%> +
+ <% if(application) {%> +
+
+ +
+
+ <% } %> +
+ +
+
+ <%-application.name%> +
+
<%-application.name || "Unknown App"%>
+
+
+ +
+
+
<%- ratingsData && ratingsData.averageRating ? ratingsData.averageRating : 0 %>
+
<%- e('outOf5') %>
+
+
+
+ <% var avgRating = ratingsData && ratingsData.averageRating ? ratingsData.averageRating : 0; %> + <% for(var i = 1; i <= 5; i++) { %> + + <% } %> +
+
<%- ratingsData && ratingsData.totalRatings ? ratingsData.totalRatings : 0 %> <%- e('ratings') %>
+
+
+ + <% if(ratingsData && ratingsData.distribution && ratingsData.distribution.length > 0) { %> +
+ <% ratingsData.distribution.forEach(function(item) { %> +
+
+ <% for(var i = 0; i < item.stars; i++) { %> + + <% } %> +
+
+
+
+
<%- item.percentage %>%
+
+ <% }) %> +
+ <% } %> + + <% if(canWriteReview) { %> +
+ <% if(hasUserRated) { %> +
+
+ <%- e('yourRating') %>: +
+ <% for(var i = 1; i <= 5; i++) { %> + + <% } %> +
+ <%- userRating %> <%- e('outOf5') %> +
+
<%- e('youAlreadyRated') %>
+
+ <% } else { %> + + <% } %> + +
+ <% } %> + +
+
+

<%- e('ratingsAndReviews') %>

+
+ + <% if(ratingsData && ratingsData.reviews && ratingsData.reviews.length > 0) { %> + <% ratingsData.reviews.forEach(function(review) { %> +
+
+
+
+ <% if(!review.userAvatar) { %> + + <% } else { %> + + <% } %> +
+
+
<%- review.userName %>
+
<%- review.date %>
+
+
+ <% if(userAddress && review.address === userAddress) { %> +
+ +
+ <% } %> +
+ +
+ <% if(review.title) { %> + <%- review.title %>
+ <% } %> + <%- review.text %> +
+
+ <% }) %> + <% } else { %> +
+

<%- e('noReviewsYet') %>

+
+ <% } %> +
+
diff --git a/js/kit.js b/js/kit.js index a5fed22bb..b3ab92107 100644 --- a/js/kit.js +++ b/js/kit.js @@ -2929,6 +2929,21 @@ pMiniapp = function(){ } + self.upvote = function(value, address){ + + if(self.myVal && self.myVal != '0') return null; + + var upvoteShare = new UpvoteShare(); + + upvoteShare.share.set(self.hash); + upvoteShare.value.set(value); + upvoteShare.address.set(self.address || '') + + + return upvoteShare; + } + + self.type = 'miniapp'; return self; diff --git a/js/satolist.js b/js/satolist.js index 385bb31fe..55a27f4d6 100644 --- a/js/satolist.js +++ b/js/satolist.js @@ -16457,12 +16457,12 @@ Platform = function (app, listofnodes) { - getclear: function (txid, pid, clbk, ccha) { + getclear: function (txid, pid, clbk, options) { var parameters = [txid, pid || '', self.app.user.address.value || ''] self.psdk.comment.request(() => { - return self.app.api.rpc('getcomments', parameters) + return self.app.api.rpc('getcomments', parameters, options) }, { parameters }).then(d => { diff --git a/localization/en.js b/localization/en.js index 6adf40788..ceaf96b38 100644 --- a/localization/en.js +++ b/localization/en.js @@ -3278,3 +3278,30 @@ _l.addToCollection = "Add to collection" _l.e13163 = "There are no changes in the collection" _l.imageerror = "An error occurred while loading images. Please try again" + +_l.ratingsAndReviews = "Ratings & Reviews" +_l.allReviews = "All Reviews" +_l.rateApp = "Rate" +_l.writeComment = "Write Comment" +_l.yourRating = "Your rating" +_l.youAlreadyRated = "You have already rated this app" +_l.noReviewsYet = "No reviews yet. Be the first to leave a comment!" +_l.noRatingsYet = "No ratings yet" +_l.beFirstToRate = "Install the app and be the first to rate it!" +_l.commentMinLength = "Comment must be at least 10 characters" +_l.commentSent = "Comment sent! It will appear shortly after being processed on the blockchain." +_l.ratingSent = "Rating sent! It will appear shortly after being processed on the blockchain." +_l.commentSendError = "Error sending comment. Please try again." +_l.tellAboutExperience = "Tell us about your experience with the app..." +_l.yourComment = "Your comment" +_l.submitReview = "Submit Review" +_l.cancel = "Cancel" +_l.howDoYouRate = "How do you rate this app?" +_l.clickStarToRate = "Click on a star to rate" +_l.outOf5 = "out of 5" +_l.ratings = "ratings" +_l.rating = "rating" +_l.submitRating = "Rate" +_l.mustInstallToComment = "You must install the app before writing a review" +_l.mustInstallToRate = "You must install the app before rating" +_l.reviewsAndRatingsMenu = "Reviews and Ratings" diff --git a/localization/ru.js b/localization/ru.js index 2e93800a4..775c769e1 100644 --- a/localization/ru.js +++ b/localization/ru.js @@ -3092,3 +3092,30 @@ _l.e131632 = "В коллекцию не внесено изменений" _l.imageerror = "Произошла ошибка при загрузке изображений. Попробуйте ещё раз." +_l.ratingsAndReviews = "Оценки и отзывы" +_l.allReviews = "Все отзывы" +_l.rateApp = "Оценить" +_l.writeComment = "Написать комментарий" +_l.yourRating = "Ваша оценка" +_l.youAlreadyRated = "Вы уже оценили это приложение" +_l.noReviewsYet = "Пока нет отзывов. Станьте первым, кто оставит комментарий!" +_l.noRatingsYet = "Оценок пока нет" +_l.beFirstToRate = "Установите приложение и станьте первым, кто его оценит!" +_l.commentMinLength = "Комментарий должен содержать минимум 10 символов" +_l.commentSent = "Комментарий отправлен! Он появится в ближайшее время после обработки в блокчейне." +_l.ratingSent = "Оценка отправлена! Она появится в ближайшее время после обработки в блокчейне." +_l.commentSendError = "Ошибка отправки комментария. Попробуйте снова." +_l.tellAboutExperience = "Расскажите о вашем опыте использования приложения..." +_l.yourComment = "Ваш комментарий" +_l.submitReview = "Отправить отзыв" +_l.cancel = "Отмена" +_l.howDoYouRate = "Как вы оцениваете это приложение?" +_l.clickStarToRate = "Нажмите на звезду для оценки" +_l.outOf5 = "из 5" +_l.ratings = "оценок" +_l.rating = "оценка" +_l.submitRating = "Оценить" +_l.mustInstallToComment = "Вы должны установить приложение перед написанием отзыва" +_l.mustInstallToRate = "Вы должны установить приложение перед оценкой" +_l.reviewsAndRatingsMenu = "Отзывы и оценки" +