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) { %> +
<%- e('noReviewsYet') %>
+