summaryrefslogtreecommitdiff
path: root/src/components/jet/item/ReviewItem.svelte
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /src/components/jet/item/ReviewItem.svelte
init commit
Diffstat (limited to 'src/components/jet/item/ReviewItem.svelte')
-rw-r--r--src/components/jet/item/ReviewItem.svelte237
1 files changed, 237 insertions, 0 deletions
diff --git a/src/components/jet/item/ReviewItem.svelte b/src/components/jet/item/ReviewItem.svelte
new file mode 100644
index 0000000..7f406c8
--- /dev/null
+++ b/src/components/jet/item/ReviewItem.svelte
@@ -0,0 +1,237 @@
+<script lang="ts">
+ import type { Review as ReviewModel } from '@jet-app/app-store/api/models';
+
+ import LineClamp from '@amp/web-app-components/src/components/LineClamp/LineClamp.svelte';
+ import Modal from '@amp/web-app-components/src/components/Modal/Modal.svelte';
+ import ContentModal from '~/components/jet/item/ContentModal.svelte';
+ import Truncate from '@amp/web-app-components/src/components/Truncate/Truncate.svelte';
+ import StarRating from '~/components/StarRating.svelte';
+ import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
+ import { getI18n } from '~/stores/i18n';
+ import { getJet } from '~/jet/svelte';
+ import {
+ escapeHtml,
+ stripUnicodeWhitespace,
+ } from '~/utils/string-formatting';
+ import { CUSTOMER_REVIEW_MODAL_ID } from '~/utils/metrics';
+
+ export let item: ReviewModel;
+ export let isDetailView: boolean = false;
+
+ let modalComponent: Modal | undefined;
+ let modalTriggerElement: HTMLElement | null = null;
+
+ const jet = getJet();
+ const i18n = getI18n();
+ const translateFn = (key: string) => $i18n.t(key);
+
+ const handleCloseModal = () => modalComponent?.close();
+ const handleOpenModal = () => {
+ modalComponent?.showModal();
+ jet.recordCustomMetricsEvent({
+ eventType: 'dialog',
+ dialogId: 'more',
+ targetId: CUSTOMER_REVIEW_MODAL_ID,
+ dialogType: 'button',
+ });
+ };
+
+ $: ({ id, reviewerName, rating, contents, title, date, response } = item);
+ $: dateForDisplay = jet.localization.timeAgo(new Date(date));
+ $: dateForAttribute = new Date(date).toISOString();
+ $: titleId = `review-${id}-title`;
+ $: maximumLinesForReview = response ? 3 : 5;
+ $: responseDateForDisplay =
+ response && jet.localization.timeAgo(new Date(response.date));
+ $: responseDateForAttribute =
+ response && new Date(response.date).toISOString();
+ $: reviewContents = stripUnicodeWhitespace(escapeHtml(contents));
+ $: responseContents =
+ response && stripUnicodeWhitespace(escapeHtml(response.contents));
+</script>
+
+<article class:is-detail-view={isDetailView} aria-labelledby={titleId}>
+ <div class="header">
+ <div class="title-and-rating-container">
+ {#if !isDetailView}
+ <h3 id={titleId} class="title">
+ <LineClamp clamp={1}>
+ {title}
+ </LineClamp>
+ </h3>
+ {/if}
+
+ <StarRating
+ {rating}
+ --fill-color="var(--systemOrange)"
+ --star-size={isDetailView ? '24px' : '12px'}
+ />
+ </div>
+
+ <div class="review-header">
+ <time class="date" datetime={dateForAttribute}>
+ {dateForDisplay}
+ </time>
+
+ <LineClamp clamp={1}>
+ <p class="author">
+ {reviewerName}
+ </p>
+ </LineClamp>
+ </div>
+ </div>
+
+ {#if isDetailView}
+ <p>
+ {@html sanitizeHtml(reviewContents, {
+ allowedTags: [''],
+ keepChildrenWhenRemovingParent: true,
+ })}
+
+ {#if response}
+ <div class="developer-response-container">
+ <div class="developer-response-header">
+ <span class="developer-response-heading">
+ {$i18n.t(
+ 'ASE.Web.AppStore.Review.DeveloperResponse',
+ )}
+ </span>
+
+ <time class="date" datetime={responseDateForAttribute}>
+ {responseDateForDisplay}
+ </time>
+ </div>
+
+ {@html sanitizeHtml(responseContents, {
+ allowedTags: [''],
+ keepChildrenWhenRemovingParent: true,
+ })}
+ </div>
+ {/if}
+ </p>
+ {:else}
+ <div class="content">
+ <Truncate
+ on:openModal={handleOpenModal}
+ {title}
+ lines={maximumLinesForReview}
+ {translateFn}
+ text={reviewContents}
+ isPortalModal={true}
+ />
+
+ {#if item.response}
+ <div class="developer-response-container">
+ <span class="developer-response-heading">
+ {$i18n.t('ASE.Web.AppStore.Review.DeveloperResponse')}
+ </span>
+ <Truncate
+ on:openModal={handleOpenModal}
+ {title}
+ {translateFn}
+ lines={1}
+ text={responseContents}
+ isPortalModal={true}
+ />
+ </div>
+ {/if}
+ </div>
+ {/if}
+</article>
+
+{#if !isDetailView}
+ <Modal {modalTriggerElement} bind:this={modalComponent}>
+ <ContentModal
+ on:close={handleCloseModal}
+ {title}
+ subtitle={null}
+ targetId={CUSTOMER_REVIEW_MODAL_ID}
+ >
+ <svelte:fragment slot="content">
+ <svelte:self {item} isDetailView={true} />
+ </svelte:fragment>
+ </ContentModal>
+ </Modal>
+{/if}
+
+<style lang="scss">
+ article:not(.is-detail-view) {
+ height: 186px;
+ padding: 20px 16px;
+ background-color: var(--systemQuinary);
+ border-radius: var(--global-border-radius-xlarge);
+
+ @media (--small) {
+ padding: 20px;
+ }
+ }
+
+ .header {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 18px;
+ align-items: center;
+ justify-content: space-between;
+
+ .is-detail-view & {
+ margin-bottom: 0;
+ }
+ }
+
+ .title-and-rating-container {
+ .is-detail-view & {
+ display: flex;
+ }
+ }
+
+ .title {
+ color: var(--systemPrimary);
+ font: var(--body-emphasized);
+ margin-bottom: 4px;
+ }
+
+ .date,
+ .author {
+ color: var(--systemSecondary);
+ font: var(--callout);
+ word-break: normal;
+ }
+
+ .content {
+ position: relative;
+ word-wrap: break-word; /* Break to fit the review block, even when people leave a review with long text without spaces */
+ text-align: start;
+ font: var(--body);
+ }
+
+ .review-header {
+ text-align: end;
+ }
+
+ .developer-response-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 5px;
+ margin-top: 20px;
+ }
+
+ .developer-response-heading {
+ font: var(--body-emphasized);
+
+ .is-detail-view & {
+ display: block;
+ font: var(--title-3-emphasized);
+ }
+ }
+
+ .developer-response-container {
+ margin-top: 10px;
+ }
+
+ article :global(.more) {
+ --moreTextColorOverride: var(--keyColor);
+ --moreFontOverride: var(--body);
+ text-transform: lowercase;
+ }
+</style>