diff options
Diffstat (limited to 'src/components/pages')
| -rw-r--r-- | src/components/pages/AppEventDetailPage.svelte | 44 | ||||
| -rw-r--r-- | src/components/pages/ArticlePage.svelte | 141 | ||||
| -rw-r--r-- | src/components/pages/ChartsHubPage.svelte | 11 | ||||
| -rw-r--r-- | src/components/pages/DefaultPage.svelte | 173 | ||||
| -rw-r--r-- | src/components/pages/ErrorPage.svelte | 23 | ||||
| -rw-r--r-- | src/components/pages/ProductPage.svelte | 77 | ||||
| -rw-r--r-- | src/components/pages/SearchLandingPage.svelte | 33 | ||||
| -rw-r--r-- | src/components/pages/SearchResultsPage.svelte | 113 | ||||
| -rw-r--r-- | src/components/pages/SeeAllPage.svelte | 56 | ||||
| -rw-r--r-- | src/components/pages/StaticMessagePage.svelte | 113 | ||||
| -rw-r--r-- | src/components/pages/TodayPage.svelte | 22 | ||||
| -rw-r--r-- | src/components/pages/TopChartsPage.svelte | 218 | ||||
| -rw-r--r-- | src/components/pages/VisionProPage.svelte | 12 |
13 files changed, 1036 insertions, 0 deletions
diff --git a/src/components/pages/AppEventDetailPage.svelte b/src/components/pages/AppEventDetailPage.svelte new file mode 100644 index 0000000..a2b798e --- /dev/null +++ b/src/components/pages/AppEventDetailPage.svelte @@ -0,0 +1,44 @@ +<script lang="ts" context="module"> + import type { DefaultPageRequirements } from './DefaultPage.svelte'; +</script> + +<script lang="ts"> + import ShelfComponent from '~/components/jet/shelf/Shelf.svelte'; + + export let page: DefaultPageRequirements; +</script> + +<div class="app-event-detail-page-container"> + <div class="shelf-container"> + {#each page.shelves as shelf} + <ShelfComponent {shelf} /> + {/each} + </div> +</div> + +<style> + .app-event-detail-page-container { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 0 var(--bodyGutter); + } + + .shelf-container { + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + max-width: 900px; + margin: 0 auto; + + @media (--range-small-up) { + justify-content: center; + } + } + + .shelf-container :global(.shelf) { + margin: 0; + padding: var(--bodyGutter) 0 0; + } +</style> diff --git a/src/components/pages/ArticlePage.svelte b/src/components/pages/ArticlePage.svelte new file mode 100644 index 0000000..32cacb0 --- /dev/null +++ b/src/components/pages/ArticlePage.svelte @@ -0,0 +1,141 @@ +<script lang="ts" context="module"> + import type { ArticlePage } from '@jet-app/app-store/api/models'; + + import type { DefaultPageRequirements } from './DefaultPage.svelte'; + + /** + * Just the `Page` props that we actually need to render this component + */ + export type ArticlePageRequirements = DefaultPageRequirements & { + card: ArticlePage['card']; + footerLockup: ArticlePage['footerLockup']; + }; +</script> + +<script lang="ts"> + import TodayCard from '~/components/jet/today-card/TodayCard.svelte'; + import ShelfComponent from '~/components/jet/shelf/Shelf.svelte'; + import FooterLockupItem from '~/components/jet/item/FooterLockupItem.svelte'; + export let page: ArticlePageRequirements; + + $: ({ card } = page); +</script> + +<div class="article-page-container" data-testid="article-page-container"> + <div class="article-layout"> + {#if card} + <div class="card-container"> + <TodayCard {card} suppressClickAction /> + </div> + {/if} + + <div class="story-container"> + {#each page.shelves as shelf} + {#if !shelf.isHidden} + <ShelfComponent {shelf} /> + {/if} + {/each} + + {#if page.footerLockup} + <div class="footer-lockup-container"> + <FooterLockupItem item={page.footerLockup} /> + </div> + {/if} + </div> + </div> +</div> + +<style lang="scss"> + @use 'ac-sasskit/modules/viewportcontent/core' as *; + @use 'amp/stylekit/core/viewports' as *; + + .article-page-container { + flex-grow: 1; + width: 100%; + margin: 0 auto; + } + + .article-layout { + --article-page-padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--article-page-padding); + max-width: 1600px; + margin: 0 auto; + + @media (--range-small-up) { + padding: 2em var(--bodyGutter); + } + + @media (--range-small-only) { + --article-page-padding: 40px; + } + + @media (--range-medium-up) { + align-items: flex-start; + flex-direction: row; + } + + @media (--range-medium-only) { + --article-page-padding: 20px; + } + + @media (--range-large-up) { + --article-page-padding: 40px; + } + } + + .card-container { + flex-shrink: 0; + aspect-ratio: 3/4; + width: 100%; + + @media (--range-xsmall-only) { + --border-radius: 0; + } + + @media (--range-small-only) { + aspect-ratio: 16/9; + } + + @media (--range-small-up) { + width: 100%; + } + + @media (--range-medium-up) { + position: sticky; + top: 2em; + aspect-ratio: 3 / 4; + height: min(calc(100vh - 80px), calc(33vw * 4 / 3)); + min-height: 420px; + max-height: 700px; + width: auto; + } + } + + .story-container { + width: 100%; + margin-top: 20px; + padding-bottom: var(--bodyGutter); + + @media (--range-small-up) { + width: calc(100%); + margin-top: 0; + } + + @media (--range-medium-up) { + min-width: calc(50% - calc(var(--article-page-padding))); + } + } + + .story-container :global(.shelf:first-of-type) { + padding-top: 0; + padding-bottom: 13px; + } + + .footer-lockup-container { + margin: var(--bodyGutter); + } +</style> diff --git a/src/components/pages/ChartsHubPage.svelte b/src/components/pages/ChartsHubPage.svelte new file mode 100644 index 0000000..a75cb64 --- /dev/null +++ b/src/components/pages/ChartsHubPage.svelte @@ -0,0 +1,11 @@ +<script lang="ts"> + import type { ChartsHubPage } from '@jet-app/app-store/api/models'; + + import TopChartsPage from './TopChartsPage.svelte'; + + export let page: ChartsHubPage; +</script> + +{#each page.charts as chart} + <TopChartsPage page={chart} /> +{/each} diff --git a/src/components/pages/DefaultPage.svelte b/src/components/pages/DefaultPage.svelte new file mode 100644 index 0000000..7905b07 --- /dev/null +++ b/src/components/pages/DefaultPage.svelte @@ -0,0 +1,173 @@ +<script lang="ts" context="module"> + import type { + PagePresentationOptions, + Shelf, + } from '@jet-app/app-store/api/models'; + import type { WebRenderablePage } from '@jet-app/app-store/api/models/web-renderable-page'; + + /** + * Just the `Page` props that we actually need to render this component + */ + export interface DefaultPageRequirements extends WebRenderablePage { + shelves: Shelf[]; + presentationOptions?: PagePresentationOptions; + } +</script> + +<script lang="ts"> + import type { MarkerShelf } from '~/components/jet/shelf/MarkerShelf.svelte'; + import { isUberShelf } from '~/components/jet/shelf/UberShelf.svelte'; + import ShelfComponent from '~/components/jet/shelf/Shelf.svelte'; + import { partition } from '~/utils/array'; + import { carouselMediaStyle } from '~/stores/carousel-media-style'; + import mediaQueries from '~/utils/media-queries'; + import { isHeroCarouselShelf } from '../jet/shelf/HeroCarouselShelf.svelte'; + import { isRtl } from '~/utils/locale'; + + interface $$Slots { + 'before-shelves': {}; + + /** + * If {@linkcode ShelfComponent}` recognizes a shelf to be a {@linkcode MarkerShelf}, + * this slot will be rendered so that the "page" data can be supplied by a "parent" + * component + */ + 'marker-shelf': { + shelf: MarkerShelf; + }; + } + + export let page: DefaultPageRequirements; + + $: ({ title, presentationOptions = [] } = page); + + // Some shelves are meant to be rendered above the title, rather than below it + $: [aboveTitleShelves, belowTitleShelves] = partition( + page.shelves, + (shelf) => { + // Some "uber" shelves might be placed above the title + if (isUberShelf(shelf)) { + const [uber] = shelf.items; + return uber.style === 'above'; + } + + // Everything else should be below it + return false; + }, + ); + + $: prefersHiddenPageTitle = presentationOptions.includes( + 'prefersHiddenPageTitle', + ); + $: prefersLargeTitle = presentationOptions.includes('prefersLargeTitle'); + $: prefersOverlayedPageHeader = + $mediaQueries === 'xsmall' && + presentationOptions.includes('prefersOverlayedPageHeader'); + $: isOnDarkBackground = + prefersOverlayedPageHeader && $carouselMediaStyle === 'dark'; + + $: isTitleDuplicatedInHero = (() => { + const firstShelf = page.shelves?.[0]; + + if ( + !firstShelf || + !isHeroCarouselShelf(firstShelf) || + firstShelf.items?.length !== 1 + ) { + return false; + } + + const { items: ltrItems, rtlItems } = firstShelf.items?.[0] ?? {}; + const firstItem = isRtl() && rtlItems?.length ? rtlItems : ltrItems; + const firstTitle = firstItem?.[0]?.overlay?.titleText; + + return title === firstTitle; + })(); +</script> + +<div + class="default-page-container" + data-testid="default-page-container" + class:with-overlaid-title={prefersOverlayedPageHeader} + class:with-title-in-hero={isTitleDuplicatedInHero} +> + {#each aboveTitleShelves as shelf} + <ShelfComponent {shelf}> + <slot name="marker-shelf" slot="marker-shelf" let:shelf {shelf} /> + </ShelfComponent> + {/each} + + {#if title && !prefersHiddenPageTitle && !isTitleDuplicatedInHero} + <h1 + data-test-id="page-title" + class:large-title={prefersLargeTitle} + class:overlaid={prefersOverlayedPageHeader} + class:on-dark-background={isOnDarkBackground} + > + {title} + </h1> + {/if} + + <slot name="before-shelves" /> + + {#each belowTitleShelves as shelf} + {#if !shelf.isHidden} + <ShelfComponent {shelf}> + <slot + name="marker-shelf" + slot="marker-shelf" + let:shelf + {shelf} + /> + </ShelfComponent> + {/if} + {/each} +</div> + +<style lang="scss"> + @use 'ac-sasskit/modules/viewportcontent/core' as *; + @use 'amp/stylekit/core/viewports' as *; + + .default-page-container { + flex-grow: 1; + width: 100%; + max-width: viewport-content-for(xlarge); + margin: 0 auto; + } + + .default-page-container.with-overlaid-title { + margin-top: -13px; + } + + .default-page-container.with-title-in-hero { + @media (--range-small-up) { + margin-top: 10px; + } + } + + h1 { + padding: 11px var(--bodyGutter); + font: var(--large-title-emphasized); + letter-spacing: -0.5px; + word-wrap: break-word; + color: var(--systemPrimary, #000); + position: relative; + z-index: 1; + transition: color 210ms ease-in; + } + + h1.large-title { + font: var(--large-title-emphasized-tall); + } + + h1.overlaid { + position: absolute; + z-index: 3; + padding: var(--bodyGutter) var(--bodyGutter) 0; + color: var(--systemPrimary-onLight, #000); + } + + h1.on-dark-background { + color: var(--systemPrimary-onDark); + } +</style> diff --git a/src/components/pages/ErrorPage.svelte b/src/components/pages/ErrorPage.svelte new file mode 100644 index 0000000..5756d78 --- /dev/null +++ b/src/components/pages/ErrorPage.svelte @@ -0,0 +1,23 @@ +<script lang="ts" context="module"> + import type { ErrorPage } from '~/jet/models'; +</script> + +<script lang="ts"> + import SharedErrorPage from '@amp/web-app-components/src/components/Error/ErrorPage.svelte'; + import { getI18n } from '~/stores/i18n'; + + export let page: ErrorPage; + + const i18n = getI18n(); +</script> + +<div class="error-page-container"> + <SharedErrorPage translateFn={$i18n.t} error={page.error} /> +</div> + +<style> + .error-page-container :global(.page-error) { + /* -50px compensates for the global footer */ + top: calc(50% - 50px); + } +</style> diff --git a/src/components/pages/ProductPage.svelte b/src/components/pages/ProductPage.svelte new file mode 100644 index 0000000..30b0ad8 --- /dev/null +++ b/src/components/pages/ProductPage.svelte @@ -0,0 +1,77 @@ +<script lang="ts"> + import type { ShelfBasedProductPage } from '@jet-app/app-store/api/models'; + import { isFlowAction } from '@jet-app/app-store/api/models'; + import type { WebRenderablePage } from '@jet-app/app-store/api/models/web-renderable-page'; + + import DefaultPage, { + type DefaultPageRequirements, + } from '~/components/pages/DefaultPage.svelte'; + import MarkerShelf from '~/components/jet/shelf/MarkerShelf.svelte'; + import ProductPageArcadeFooter from '~/components/ProductPageArcadeFooter.svelte'; + import { getProductPageShelvesWithExpandedMedia } from '~/utils/shelves'; + import { setAccessibilityLayoutContext } from '~/context/accessibility-layout'; + import { getJet } from '~/jet'; + import { isProductPageLinkShelf } from '~/components/jet/shelf/ProductPageLinkShelf.svelte'; + import { isEulaPageIntent } from '@jet-app/app-store/api/intents/eula-page-intent'; + export let page: ShelfBasedProductPage & WebRenderablePage; + + const jet = getJet(); + + $: ({ presentationOptions, webNavigation } = page); + + $: shelves = getProductPageShelvesWithExpandedMedia(page); + + let defaultPageRequirements: DefaultPageRequirements; + + $: defaultPageRequirements = { + shelves, + presentationOptions, + webNavigation, + }; + + // Set up accessibility layout context for neighbor shelf detection + $: { + setAccessibilityLayoutContext({ shelves }); + + /** + * We suppport "deep linking" to the product page with the License Agreement modal open by + * default, based on the presence of the `lic` query parameter. No other modals support + * opening via deep link, which is why there isn't a more robust solution for this use case. + * Instead, we are just firing off the click action from the license agreement shelf. + */ + if (page.canonicalURL) { + const canonicalUrl = new URL(page.canonicalURL); + const hasLic = canonicalUrl.searchParams.has('lic'); + + if (hasLic && shelves) { + const eulaItem = shelves + .find(isProductPageLinkShelf) + ?.items.find( + ({ clickAction }) => + isFlowAction(clickAction) && + clickAction.destination && + isEulaPageIntent(clickAction.destination), + ); + + if (eulaItem) { + jet.perform(eulaItem.clickAction); + } + } + } + } + + // TODO: replace with `supportsArcade` from Jet + // rdar://143706610 (Support `supportsArcade` attribute) + $: supportsArcade = + page.lockup.offerDisplayProperties?.offerType === 'arcadeApp'; +</script> + +<DefaultPage page={defaultPageRequirements}> + <svelte:fragment slot="marker-shelf" let:shelf> + <MarkerShelf {shelf} {page} /> + </svelte:fragment> +</DefaultPage> + +{#if supportsArcade} + <ProductPageArcadeFooter /> +{/if} diff --git a/src/components/pages/SearchLandingPage.svelte b/src/components/pages/SearchLandingPage.svelte new file mode 100644 index 0000000..3594ece --- /dev/null +++ b/src/components/pages/SearchLandingPage.svelte @@ -0,0 +1,33 @@ +<script lang="ts"> + import type { SearchLandingPage } from '@jet-app/app-store/api/models'; + import type { WebRenderablePage } from '@jet-app/app-store/api/models/web-renderable-page'; + import type { WebSearchFlowAction } from '@jet-app/app-store/common/search/web-search-action'; + import { unwrapOptional as unwrap } from '@jet/environment/types/optional'; + + type SearchPage = SearchLandingPage; + + import DefaultPage from './DefaultPage.svelte'; + import ShelfWrapper from '~/components/Shelf/Wrapper.svelte'; + import SearchInput from '~/components/navigation/SearchInput.svelte'; + import { getI18n } from '~/stores/i18n'; + + export let page: SearchPage; + + const i18n = getI18n(); + + $: webNavigation = unwrap((page as WebRenderablePage).webNavigation); + $: searchAction = webNavigation.searchAction as WebSearchFlowAction; + $: hasShelves = !!page.shelves.filter(({ items }) => items?.length).length; + + $: pageWithoutEmptyShelves = { + ...page, + shelves: hasShelves ? page.shelves : [], + title: $i18n.t('ASE.Web.AppStore.Meta.SearchLanding.Title'), + }; +</script> + +<DefaultPage page={pageWithoutEmptyShelves}> + <ShelfWrapper slot="before-shelves" centered> + <SearchInput {searchAction} big /> + </ShelfWrapper> +</DefaultPage> diff --git a/src/components/pages/SearchResultsPage.svelte b/src/components/pages/SearchResultsPage.svelte new file mode 100644 index 0000000..c17b644 --- /dev/null +++ b/src/components/pages/SearchResultsPage.svelte @@ -0,0 +1,113 @@ +<script lang="ts"> + import type { SearchResultsPage } from '@jet-app/app-store/api/models'; + + import type { Size } from '@amp/web-app-components/src/types'; + import { ShelfConfig } from '@amp/web-app-components/config/components/shelf'; + + import DefaultPage from './DefaultPage.svelte'; + import { getI18n } from '~/stores/i18n'; + import mediaQueries from '~/utils/media-queries'; + import { + isSearchResultShelf, + isRenderableInSearchResultsShelf, + } from '~/components/jet/shelf/SearchResultShelf.svelte'; + import { getPlatformFromPage } from '~/utils/seo/common'; + + export let page: SearchResultsPage; + + const i18n = getI18n(); + + $: resultsShelf = page?.shelves?.find(isSearchResultShelf) ?? null; + + $: renderableItems = (resultsShelf?.items ?? []).filter( + isRenderableInSearchResultsShelf, + ); + + $: columnConfig = ShelfConfig.get().GRID_VALUES.SearchResult; + $: numberOfColumns = columnConfig[$mediaQueries as Size] || 3; + $: numberOfRows = Math.ceil(renderableItems.length / numberOfColumns); + $: middleRow = Math.floor(numberOfRows / 2); + $: insertAt = middleRow * numberOfColumns; + + /** + * This is unfortunate but only these three platforms support the transparency link. + * This link is enabled via the `transparencyLawEditorialItemId` bag key, but when defining + * bag keys, we do not have access to the platform being viewed, so we can't opt-out there. + * We could do this platform check in the Jet layer, but adding two forms of opting into this + * link felt cumbersome and unintuitive, so we can just do it here. + */ + $: transparencyLink = + page.transparencyLink && + ['iphone', 'ipad', 'mac'].includes( + getPlatformFromPage(page).toLowerCase(), + ); + + /** + * Here we are building constructing a new array of shelves _if_ there is a result shelf _and_ + * a transparency link. This creates three shelves: + * 1) the search results before the transparency banner in the linkable text shelf + * 2) the transparency banner + * 3) the search results after the transparency banner + */ + $: shelves = resultsShelf + ? transparencyLink && renderableItems.length + ? [ + insertAt > 0 && { + ...resultsShelf, + items: renderableItems.slice(0, insertAt), + title: null, + isValid: () => true, + }, + { + contentType: 'linkableText', + items: [page.transparencyLink], + }, + { + ...resultsShelf, + items: renderableItems.slice(insertAt), + title: null, + isValid: () => true, + }, + ] + : [{ ...resultsShelf, items: renderableItems, title: null }] + : []; +</script> + +<DefaultPage + page={{ + shelves, + title: renderableItems.length > 0 ? resultsShelf?.title : null, + }} +> + <svelte:fragment slot="before-shelves"> + {#if renderableItems.length === 0} + <div> + <h1> + {$i18n.t('ASE.Web.AppStore.Search.NoResults.FirstLine')} + </h1> + <p> + {$i18n.t('ASE.Web.AppStore.Search.NoResults.SecondLine', { + term: page.searchTermContext?.term, + })} + </p> + </div> + {/if} + </svelte:fragment> +</DefaultPage> + +<style> + div { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 3px; + height: 70vh; + margin: var(--bodyGutter); + } + + p { + font: var(--title-3); + color: var(--systemSecondary); + } +</style> diff --git a/src/components/pages/SeeAllPage.svelte b/src/components/pages/SeeAllPage.svelte new file mode 100644 index 0000000..d401f32 --- /dev/null +++ b/src/components/pages/SeeAllPage.svelte @@ -0,0 +1,56 @@ +<script lang="ts"> + import type { SeeAllPage } from '@jet-app/app-store/api/models'; + import type { WebRenderablePage } from '@jet-app/app-store/api/models/web-renderable-page'; + + import DefaultPage from '~/components/pages/DefaultPage.svelte'; + import { getProductPageShelvesForOrdering } from '~/utils/shelves'; + import { setAccessibilityLayoutContext } from '~/context/accessibility-layout'; + import { isSmallLockupShelf } from '~/components/jet/shelf/SmallLockupShelf.svelte'; + import { isProductReviewShelf } from '~/components/jet/shelf/ProductReviewShelf.svelte'; + import { isProductRatingsShelf } from '~/components/jet/shelf/ProductRatingsShelf.svelte'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + + export let page: SeeAllPage & WebRenderablePage; + + $: shelves = getProductPageShelvesForOrdering(page, 'notPurchasedOrdering') + .filter((shelf) => { + const isShelfForReviewPage = + isProductReviewShelf(shelf) || isProductRatingsShelf(shelf); + + return ( + isSmallLockupShelf(shelf) || + (isShelfForReviewPage && page.seeAllType === 'reviews') + ); + }) + .map((shelf) => { + shelf.isHorizontal = false; + shelf.seeAllAction = null; + return shelf; + }); + + $: { + setAccessibilityLayoutContext({ shelves }); + } +</script> + +<DefaultPage page={{ shelves, title: null }}> + <svelte:fragment slot="before-shelves"> + <h1> + <LinkWrapper action={page.lockup.clickAction}> + {page.lockup.title} + </LinkWrapper> + </h1> + </svelte:fragment> +</DefaultPage> + +<style> + h1 { + font: var(--title-1); + color: var(--keyColor); + margin: 13px var(--bodyGutter) 0; + } + + h1 :global(a:hover) { + text-decoration: underline; + } +</style> diff --git a/src/components/pages/StaticMessagePage.svelte b/src/components/pages/StaticMessagePage.svelte new file mode 100644 index 0000000..45c1a36 --- /dev/null +++ b/src/components/pages/StaticMessagePage.svelte @@ -0,0 +1,113 @@ +<script lang="ts" context="module"> + import type { StaticMessagePage } from '~/jet/models'; +</script> + +<script lang="ts"> + import { getI18n } from '~/stores/i18n'; + + export let page: StaticMessagePage; + + const i18n = getI18n(); +</script> + +<div class="static-message-page-container"> + <div class="static-message-text-wrapper"> + {#if page.titleLocKey} + <h1>{$i18n.t(page.titleLocKey)}</h1> + {/if} + + <section> + {#if page.contentType === 'win-back' || page.contentType === 'contingent-price'} + <p> + {$i18n.t('ASE.Web.AppStore.WinBack.Subhead')} + </p> + + <p> + <b> + {$i18n.t('ASE.Web.AppStore.WinBack.DirectionalTitle')} + </b> + </p> + + <ul> + <li> + {$i18n.t('ASE.Web.AppStore.WinBack.Update.iOS')} + </li> + <li> + {$i18n.t('ASE.Web.AppStore.WinBack.Update.macOS')} + </li> + </ul> + + <p> + {$i18n.t('ASE.Web.AppStore.WinBack.Body')} + </p> + {:else if page.contentType === 'carrier'} + <p class="carrier__instructions"> + {$i18n.t('ASE.Web.AppStore.Carrier.Update.iOS')} + </p> + <p> + {$i18n.t('ASE.Web.AppStore.Carrier.Body')} + </p> + {:else if page.contentType === 'invoice'} + <p class="invoice__instructions"> + {$i18n.t('ASE.Web.AppStore.Invoice.Body')} + </p> + {/if} + </section> + </div> +</div> + +<style lang="scss"> + @use 'ac-sasskit/modules/viewportcontent/core' as *; + @use 'amp/stylekit/core/viewports' as *; + + .static-message-page-container { + display: flex; + flex-grow: 1; + width: 100%; + max-width: viewport-content-for(xlarge); + margin: 0 auto; + align-items: center; + } + + @media (--range-sidebar-visible-up) { + .static-message-page-container { + height: 100%; + } + } + + .static-message-text-wrapper { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + width: auto; + margin: 0 auto; + } + + .static-message-page-container h1 { + padding: 13px var(--bodyGutter) 0; + font: var(--header-emphasized); + color: var(--systemPrimary, #000); + position: relative; + z-index: 1; + margin-bottom: 16px; + } + + .static-message-page-container section { + margin: 0 var(--bodyGutter); + font: var(--title-3); + } + + .static-message-page-container li { + list-style-type: disc; + } + + .static-message-page-container p, + .static-message-page-container ul { + margin-bottom: 16px; + text-wrap: pretty; + } + + .static-message-page-container ul { + padding-inline-start: 1em; + } +</style> diff --git a/src/components/pages/TodayPage.svelte b/src/components/pages/TodayPage.svelte new file mode 100644 index 0000000..3d38932 --- /dev/null +++ b/src/components/pages/TodayPage.svelte @@ -0,0 +1,22 @@ +<!-- +@component +Page component for the "Today Page" + +This is required so that the correct layout of the cards within each `TodayCardShelf` +can be computed at the page level, as the algorithm for stretching the correct cards +in each shelf requires knowledge of the previously-rendered shelf +--> +<script lang="ts"> + import type { TodayPage } from '@jet-app/app-store/api/models'; + + import DefaultPage from '~/components/pages/DefaultPage.svelte'; + import { setTodayCardLayoutContext } from '~/context/today-card-layout'; + + export let page: TodayPage; + + $: { + setTodayCardLayoutContext(page); + } +</script> + +<DefaultPage {page} /> diff --git a/src/components/pages/TopChartsPage.svelte b/src/components/pages/TopChartsPage.svelte new file mode 100644 index 0000000..4a3e7b7 --- /dev/null +++ b/src/components/pages/TopChartsPage.svelte @@ -0,0 +1,218 @@ +<script lang="ts"> + import { getI18n } from '~/stores/i18n'; + import { isSome } from '@jet/environment/types/optional'; + import type { TopChartsPage } from '@jet-app/app-store/api/models'; + + import DefaultPage from '~/components/pages/DefaultPage.svelte'; + import Shelf from '~/components/Shelf/Wrapper.svelte'; + import Artwork, { getNaturalProfile } from '~/components/Artwork.svelte'; + import Menu from '~/components/Menu.svelte'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + import SFSymbol from '~/components/SFSymbol.svelte'; + + export let page: TopChartsPage; + + const i18n = getI18n(); + + $: ({ categories, categoriesButtonTitle, segments, initialSegmentIndex } = + page); + $: segment = segments[initialSegmentIndex]; +</script> + +<DefaultPage page={{ shelves: segment.shelves, title: page.title }}> + <Shelf slot="before-shelves" centered> + <header> + <div class="dropdown-container"> + {#if categoriesButtonTitle} + <Menu options={categories}> + <svelte:fragment slot="trigger"> + <span class="menu-trigger-contents"> + {categoriesButtonTitle} + + <SFSymbol name="chevron.down" /> + </span> + </svelte:fragment> + + <svelte:fragment slot="option" let:option> + {@const { artwork, chartSelectAction, name } = + option} + + <LinkWrapper action={chartSelectAction}> + <div + class="category-menu-item" + class:active={name === + categoriesButtonTitle} + > + {#if isSome(artwork)} + <div class="artwork-container"> + <Artwork + {artwork} + profile={getNaturalProfile( + artwork, + [24], + )} + /> + </div> + {/if} + + <span>{name}</span> + </div> + </LinkWrapper> + </svelte:fragment> + </Menu> + {/if} + </div> + + <div class="segment-selector" aria-label={categoriesButtonTitle}> + {#each segments as segment, index} + {@const { segmentSelectAction } = segment} + {@const isSelected = initialSegmentIndex === index} + {@const filterLabel = $i18n.t( + isSelected + ? 'ASE.Web.AppStore.SelectedFilterApps.AX.Label' + : 'ASE.Web.AppStore.FilterApps.AX.Label', + { filterName: segment.shortName }, + )} + + <LinkWrapper + action={segmentSelectAction} + label={filterLabel} + > + <span class="segment" class:selected={isSelected}> + {segment.shortName} + </span> + </LinkWrapper> + {/each} + </div> + </header> + </Shelf> +</DefaultPage> + +<style> + header { + --pill-button-border-radius: 1000px; /* Arbitrary large value for "pill-style" rounded sides */ + --menu-item-padding: 0; + --menu-item-margin: 0 0 8px 0; + --menu-popover-padding: 12px 16px; + --menu-common-padding: 0; + --menu-trigger-border-radius: var(--pill-button-border-radius); + --menu-trigger-background-color: var(--systemPrimary-onDark); + --menu-trigger-padding: 6px 16px; + --menu-trigger-font: var(--body-semibold-tall); + --menu-popover-background-color: white; + --menu-popover-box-shadow: 10px 10px 10px 0 + var(--systemQuaternary-onLight); + --menu-popover-border-radius: 14px; + --menu-popover-border: 1px solid var(--systemQuaternary); + --menu-popover-z-index: calc(var(--z-web-chrome) + 1); + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + + @media (--range-small-up) { + display: grid; + align-items: center; + justify-items: start; + grid-template-columns: 1fr max-content 1fr; + } + + @media (prefers-color-scheme: dark) { + --menu-trigger-background-color: var(--systemQuaternary); + --menu-popover-background-color: var(--systemQuaternary-vibrant); + } + } + + .segment-selector { + display: flex; + justify-self: end; + gap: 4px; + padding: 2px; + background: var(--systemQuaternary); + border-radius: var(--pill-button-border-radius); + + @media (--range-small-up) { + align-items: center; + justify-self: center; + grid-column: 2; + } + } + + .segment-selector :global(a) { + display: contents; + } + + .segment { + border-radius: var(--pill-button-border-radius); + font: var(--body-semibold-tall); + padding: 6px 16px; + } + + .segment.selected { + background-color: var(--systemPrimary-onDark); + color: var(--systemPrimary); + + @media (prefers-color-scheme: dark) { + background-color: var(--systemQuaternary); + } + } + + .dropdown-container { + justify-self: start; + + @media (--range-small-up) { + grid-column: 1; + } + } + + .menu-trigger-contents { + display: flex; + align-items: center; + gap: 4px; + } + + .menu-trigger-contents :global(svg) { + height: 0.7em; + } + + .menu-trigger-contents :global(path:not([fill='none'])) { + fill: currentColor; + } + + .category-menu-item { + display: flex; + align-items: center; + padding: 8px; + border-radius: 10px; + height: 40px; + transition: background 150ms ease-in; + } + + .category-menu-item.active { + background: var(--systemQuinary); + } + + .category-menu-item:not(.active):hover { + background: rgba(0, 0, 0, 0.035); + } + + .artwork-container { + width: 24px; + margin-inline-end: 8px; + flex-shrink: 0; + } + + .category-menu-item span { + text-overflow: ellipsis; + overflow: hidden; + } + + .dropdown-container :global(.menu-popover) { + max-width: 600px; + width: 100%; + column-count: 2; + + @media (--range-medium-up) { + column-count: 3; + } + } +</style> diff --git a/src/components/pages/VisionProPage.svelte b/src/components/pages/VisionProPage.svelte new file mode 100644 index 0000000..c87ee09 --- /dev/null +++ b/src/components/pages/VisionProPage.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + import type { GenericPage } from '@jet-app/app-store/api/models'; + + import DefaultPage from './DefaultPage.svelte'; + import VisionProFooter from '~/components/structure/VisionProFooter.svelte'; + + export let page: GenericPage; +</script> + +<DefaultPage {page} /> + +<VisionProFooter /> |
