summaryrefslogtreecommitdiff
path: root/src/components/jet/shelf/ProductMediaShelf.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/shelf/ProductMediaShelf.svelte
init commit
Diffstat (limited to 'src/components/jet/shelf/ProductMediaShelf.svelte')
-rw-r--r--src/components/jet/shelf/ProductMediaShelf.svelte269
1 files changed, 269 insertions, 0 deletions
diff --git a/src/components/jet/shelf/ProductMediaShelf.svelte b/src/components/jet/shelf/ProductMediaShelf.svelte
new file mode 100644
index 0000000..f57fee7
--- /dev/null
+++ b/src/components/jet/shelf/ProductMediaShelf.svelte
@@ -0,0 +1,269 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ ProductMedia,
+ AppPlatform,
+ MediaType,
+ MediaPlatform,
+ } from '@jet-app/app-store/api/models';
+
+ interface ProductMediaShelf extends Shelf, ProductMedia {
+ items: ProductMedia['items'];
+ expandedMedia?: ProductMediaShelf[];
+ }
+
+ export function isProductMediaShelf(
+ shelf: Shelf,
+ ): shelf is ProductMediaShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'productMediaItem' && Array.isArray(items);
+ }
+
+ const platformToIconNameMap: Record<AppPlatform, string> = {
+ phone: 'iphone.gen2',
+ pad: 'ipad.gen2',
+ tv: 'tv',
+ watch: 'applewatch',
+ mac: 'macbook.gen2',
+ messages: 'message',
+ vision: 'visionpro',
+ };
+
+ const platformToDescriptionMap: Record<AppPlatform, string> = {
+ phone: 'AppStore.AppPlatform.Phone',
+ pad: 'AppStore.AppPlatform.Pad',
+ tv: 'AppStore.AppPlatform.TV',
+ watch: 'AppStore.AppPlatform.Watch',
+ mac: 'AppStore.AppPlatform.Mac',
+ messages: 'AppStore.AppPlatform.Messages',
+ vision: 'AppStore.AppPlatform.Vision',
+ };
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ProductMediaVisionItem from '~/components/jet/item/ProductMedia/ProductMediaVisionItem.svelte';
+ import ProductMediaPhoneItem from '~/components/jet/item/ProductMedia/ProductMediaPhoneItem.svelte';
+ import ProductMediaMacItem from '~/components/jet/item/ProductMedia/ProductMediaMacItem.svelte';
+ import ProductMediaPadItem from '~/components/jet/item/ProductMedia/ProductMediaPadItem.svelte';
+ import ProductMediaWatchItem from '~/components/jet/item/ProductMedia/ProductMediaWatchItem.svelte';
+ import ProductMediaTVItem from '~/components/jet/item/ProductMedia/ProductMediaTVItem.svelte';
+ import ChevronDown from '~/sf-symbols/chevron.down.svg';
+ import SFSymbol from '~/components/SFSymbol.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { slide } from 'svelte/transition';
+ import { getJet } from '~/jet';
+
+ export let shelf: ProductMediaShelf;
+ export let isExpandedMedia: boolean = false;
+
+ const i18n = getI18n();
+ const jet = getJet();
+
+ let appPlatform: AppPlatform | undefined;
+ let allPlatforms: MediaPlatform[] | undefined;
+ let mediaType: MediaType | undefined;
+ let hasPortraitMedia: boolean = false;
+ let shouldDisplayExpandedMedia: boolean = false;
+
+ $: {
+ if (shelf.contentsMetadata.type === 'productMedia') {
+ ({ hasPortraitMedia, allPlatforms } = shelf.contentsMetadata);
+ ({ appPlatform, mediaType } = shelf.contentsMetadata.platform);
+ }
+ }
+
+ $: allPlatformsDescription = allPlatforms
+ ?.map(({ appPlatform }) =>
+ $i18n.t(platformToDescriptionMap[appPlatform]),
+ )
+ ?.join($i18n.t('AppStore.AppPlatform.Component.Separator'));
+
+ $: shouldShowPlatform =
+ isExpandedMedia ||
+ shouldDisplayExpandedMedia ||
+ allPlatforms?.length === 1;
+
+ const displayExpandedMedia = () => {
+ shouldDisplayExpandedMedia = true;
+ jet.recordCustomMetricsEvent({
+ eventType: 'click',
+ actionDetails: { type: 'platformSelect' },
+ targetType: 'button',
+ targetId: 'productMediaShelf',
+ });
+ };
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={!shelf.expandedMedia}>
+ {#if appPlatform === 'vision'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotVision" let:item>
+ <ProductMediaVisionItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'phone' || appPlatform === 'messages'}
+ <ShelfItemLayout
+ {shelf}
+ gridType={hasPortraitMedia ? 'ScreenshotPhone' : 'ScreenshotLarge'}
+ let:item
+ >
+ <ProductMediaPhoneItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'pad'}
+ <ShelfItemLayout
+ {shelf}
+ gridType={hasPortraitMedia ? 'ScreenshotPad' : 'ScreenshotLarge'}
+ let:item
+ >
+ <ProductMediaPadItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'mac'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotLarge" let:item>
+ <ProductMediaMacItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'tv'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotLarge" let:item>
+ <ProductMediaTVItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'watch'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotPhone" let:item>
+ <ProductMediaWatchItem {item} {mediaType} />
+ </ShelfItemLayout>
+ {:else}
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ <ProductMediaPhoneItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {/if}
+
+ {#if appPlatform && shouldShowPlatform}
+ <div class="platform-description">
+ <div class="icon" aria-hidden="true">
+ <SFSymbol name={platformToIconNameMap[appPlatform]} />
+ </div>
+ <div class="platform-label">
+ {$i18n.t(platformToDescriptionMap[appPlatform])}
+ </div>
+ </div>
+ {/if}
+</ShelfWrapper>
+
+{#if shelf.expandedMedia && allPlatforms && allPlatforms.length > 1}
+ <div class="expanded-media">
+ {#if !shouldDisplayExpandedMedia}
+ <button
+ class="expanded-media-header"
+ on:click={displayExpandedMedia}
+ >
+ <div class="all-platforms">
+ <div class="all-platforms-icons">
+ {#each allPlatforms as platform}
+ <div class="icon" aria-hidden="true">
+ <SFSymbol
+ name={platformToIconNameMap[
+ platform.appPlatform
+ ]}
+ />
+ </div>
+ {/each}
+ </div>
+ <div class="all-platforms-names">
+ {allPlatformsDescription}
+ </div>
+ </div>
+ <div class="chevron-container icon" aria-hidden="true">
+ <ChevronDown />
+ </div>
+ </button>
+ {/if}
+ {#if shouldDisplayExpandedMedia}
+ <div class="expanded-media-content" transition:slide>
+ {#each shelf.expandedMedia as expandedMediaShelf}
+ <svelte:self
+ shelf={expandedMediaShelf}
+ isExpandedMedia={true}
+ />
+ {/each}
+ </div>
+ {/if}
+ </div>
+{/if}
+
+{#if !isExpandedMedia}
+ <div class="divider" />
+{/if}
+
+<style>
+ .expanded-media {
+ margin: 15px 0;
+ }
+
+ .expanded-media-header {
+ width: 100%;
+ padding-inline: var(--bodyGutter);
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .platform-description {
+ display: inline-flex;
+ align-items: center;
+ font: var(--body-reduced-semibold);
+ color: var(--systemSecondary);
+ margin-top: 15px;
+ gap: 10px;
+ margin-inline: var(--bodyGutter);
+ }
+
+ .all-platforms {
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ color: var(--systemSecondary);
+ }
+
+ .all-platforms-icons {
+ display: inline-flex;
+ gap: 10px;
+ }
+
+ .all-platforms-names {
+ font: var(--body-reduced-semibold);
+ }
+
+ .icon :global(svg) {
+ overflow: visible;
+ height: 16px;
+ max-width: 25px;
+ fill: var(--systemSecondary);
+ position: relative;
+ display: flex;
+ }
+
+ .divider {
+ margin: 10px var(--bodyGutter);
+ border-bottom: 1px solid var(--systemGray4);
+ }
+
+ .chevron-container {
+ top: 2px;
+ }
+
+ .expanded-media-content :global(.shelf:last-of-type) {
+ padding-bottom: 0;
+ }
+
+ .expanded-media-header .all-platforms,
+ .expanded-media-header .chevron-container :global(svg) {
+ transition-duration: 210ms;
+ transition-timing-function: ease-out;
+ transition-property: color, fill;
+ }
+
+ .expanded-media-header:hover .all-platforms,
+ .expanded-media-header:hover .chevron-container :global(svg) {
+ color: var(--systemPrimary);
+ fill: var(--systemPrimary);
+ }
+</style>