Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 170 additions & 48 deletions apps/megameal/PERFORMANCE_SCRATCHPAD.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/megameal/public/ads/the-dip-card-320.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/megameal/public/ads/the-dip-card-640.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/megameal/src/components/archive/ArchiveAtmosphere.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type ArchiveMediaPolicy = 'pending' | 'automatic' | 'manual' | 'reduced-motion'

export let videoSrc = '/assets/banner/archive_1.webm'
export let posterSrc = '/assets/banner/archive_still.png'
export let posterSrcset =
'/assets/banner/archive-still-1280.webp 1280w, /assets/banner/archive-still-1920.webp 1920w'
export let compactPosterSrc = '/assets/banner/home-intro-stills/archive.webp'
export let playbackRate = 0.2

Expand Down Expand Up @@ -183,6 +185,7 @@ onMount(() => {
srcset={compactPosterSrc}
type="image/webp"
/>
<source srcset={posterSrcset} sizes="100vw" type="image/webp" />
<img
src={posterSrc}
alt=""
Expand Down
16 changes: 12 additions & 4 deletions apps/megameal/src/components/store/MarketplaceListingCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import type { CollectionEntry } from 'astro:content'
import { Icon } from 'astro-icon/components'
import {
getPrimaryProductVisual,
getProductHref,
getProductListingVisual,
} from '../../contracts/products'

export interface Props {
Expand All @@ -29,7 +29,7 @@ const mediaItems =
: []),
]

const thumbnail = getPrimaryProductVisual(product)
const thumbnail = getProductListingVisual(product)

const mediaKinds = Array.from(
new Set(
Expand Down Expand Up @@ -107,7 +107,9 @@ const availabilityTone =
<Fragment>
<img
alt={product.data.name}
data-src={thumbnail}
data-src={thumbnail.src}
data-srcset={thumbnail.srcset}
data-sizes="(max-width: 720px) 10rem, (max-width: 1279px) 40vw, 19rem"
data-state="pending"
data-store-product-image
decoding="async"
Expand All @@ -120,7 +122,13 @@ const availabilityTone =
<span>Loading asset</span>
</div>
<noscript>
<img src={thumbnail} alt={product.data.name} loading="lazy" />
<img
src={thumbnail.src}
srcset={thumbnail.srcset}
sizes="(max-width: 720px) 10rem, (max-width: 1279px) 40vw, 19rem"
alt={product.data.name}
loading="lazy"
/>
</noscript>
</Fragment>
) : (
Expand Down
12 changes: 11 additions & 1 deletion apps/megameal/src/components/store/storefrontController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,17 @@ export function initStorefrontMarketplace() {
}

image.addEventListener('load', () => markState('loaded'), { once: true })
image.addEventListener('error', () => markState('error'), { once: true })
image.addEventListener('error', () => {
if (image.srcset) {
image.srcset = ''
image.removeAttribute('sizes')
image.src = source
return
}
markState('error')
})
if (image.dataset.sizes) image.sizes = image.dataset.sizes
if (image.dataset.srcset) image.srcset = image.dataset.srcset
image.src = source
}

Expand Down
8 changes: 8 additions & 0 deletions apps/megameal/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const mediaAssetSchema = z.object({
videoId: z.string().optional(),
poster: z.string().optional(),
thumbnail: z.string().optional(),
thumbnailSources: z
.array(
z.object({
src: z.string(),
width: z.number().int().positive(),
}),
)
.optional(),
alt: z.string().optional(),
caption: z.string().optional(),
lowPolyFallback: z.string().optional(),
Expand Down
5 changes: 5 additions & 0 deletions apps/megameal/src/content/products/first-contact-manual.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ rating: 4.9
media:
- type: image
src: "/posts/timeline/dont.png"
thumbnailSources:
- src: "/posts/timeline/dont-card-320.webp"
width: 320
- src: "/posts/timeline/dont-card-640.webp"
width: 640
alt: "The Interstellar Traveler's First Contact Manual cover image"
caption: "Field issue cover: the official position is still DON'T."
- type: image
Expand Down
5 changes: 5 additions & 0 deletions apps/megameal/src/content/products/megameal-cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ rating: 4.4
media:
- type: image
src: "/posts/cookbook/cookbook.png"
thumbnailSources:
- src: "/posts/cookbook/cookbook-card-320.webp"
width: 320
- src: "/posts/cookbook/cookbook-card-640.webp"
width: 640
alt: "The MegaMeal Cookbook first edition cover"
caption: "First-edition archive cover with culinary index markings intact."
- type: video
Expand Down
5 changes: 5 additions & 0 deletions apps/megameal/src/content/products/puppy-rescue-rations.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ rating: 2.8
media:
- type: image
src: "/ads/puppy-rescue.png"
thumbnailSources:
- src: "/ads/puppy-rescue-card-320.webp"
width: 320
- src: "/ads/puppy-rescue-card-640.webp"
width: 640
alt: "Puppy Rescue Rations can"
caption: "Recovered export label from a dialect region no translator will confidently certify."
specifications:
Expand Down
5 changes: 5 additions & 0 deletions apps/megameal/src/content/products/the-dip.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ rating: 4.8
media:
- type: image
src: "/ads/the-dip.png"
thumbnailSources:
- src: "/ads/the-dip-card-320.webp"
width: 320
- src: "/ads/the-dip-card-640.webp"
width: 640
alt: "The Dip product jar"
caption: "The Dip: family-format craving support from W Corporation."
- type: image
Expand Down
44 changes: 39 additions & 5 deletions apps/megameal/src/contracts/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type CardThumbnail = {
videoId?: string
}

export type ProductListingVisual = {
src: string
srcset?: string
}

export const storeCategories = [
{ id: 'all', label: 'All Products' },
{ id: 'equipment', label: 'Equipment' },
Expand Down Expand Up @@ -65,16 +70,45 @@ export function getProductHref(product: ProductEntry) {
}

export function getPrimaryProductVisual(product: ProductEntry) {
return getProductListingVisual(product)?.src
}

export function getProductListingVisual(
product: ProductEntry,
): ProductListingVisual | null {
const media = product.data.media ?? []
for (const item of media) {
if (item.type === 'image' && item.src) return resolveStoreAsset(item.src)
if (item.thumbnail) return resolveStoreAsset(item.thumbnail)
if (item.poster) return resolveStoreAsset(item.poster)
const responsiveSources = item.thumbnailSources ?? []
const srcset = responsiveSources
.map(source => `${resolveStoreAsset(source.src)} ${source.width}w`)
.join(', ')
const withResponsiveSources = (src: string): ProductListingVisual => ({
src,
...(srcset ? { srcset } : {}),
})

if (item.type === 'image' && item.src) {
return withResponsiveSources(resolveStoreAsset(item.src) ?? item.src)
}
if (item.thumbnail) {
return withResponsiveSources(
resolveStoreAsset(item.thumbnail) ?? item.thumbnail,
)
}
if (item.poster) {
return withResponsiveSources(
resolveStoreAsset(item.poster) ?? item.poster,
)
}
if (item.type === 'youtube' && item.videoId) {
return `https://img.youtube.com/vi/${item.videoId}/hqdefault.jpg`
return {
src: `https://img.youtube.com/vi/${item.videoId}/hqdefault.jpg`,
}
}
}
return resolveStoreAsset(product.data.image)

const legacyImage = resolveStoreAsset(product.data.image)
return legacyImage ? { src: legacyImage } : null
}

function isGalleryMediaType(type: string): type is MediaGalleryItem['type'] {
Expand Down
36 changes: 24 additions & 12 deletions apps/megameal/src/pages/store.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import MarketplaceListingCard from '../components/store/MarketplaceListingCard.a
import {
getAvailabilityCounts,
getCategoryCounts,
getPrimaryProductVisual,
getProductHref,
getProductListingVisual,
getProductSearchText,
productAvailabilityDisplay,
publicProductCollectionFilter,
Expand All @@ -20,12 +20,13 @@ const allProducts = sortFeaturedProductsFirst(
)

const featuredProduct = allProducts.find(p => p.data.featured)
const featuredPrimaryVisual = featuredProduct
? getPrimaryProductVisual(featuredProduct)
const featuredListingVisual = featuredProduct
? getProductListingVisual(featuredProduct)
: undefined
const spotlightProducts = allProducts
const spotlightListings = allProducts
.filter(product => product.id !== featuredProduct?.id)
.slice(0, 3)
.map(product => ({ product, visual: getProductListingVisual(product) }))

const categories = storeCategories
const categoryCounts = getCategoryCounts(allProducts)
Expand Down Expand Up @@ -64,12 +65,14 @@ const companionCount = allProducts.filter(
<aside class="storefront-featured-teaser overflow-hidden rounded-[1.35rem] border border-slate-700/70 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(2,6,23,0.96))] shadow-2xl">
<a href={getProductHref(featuredProduct)} data-sfx-hover="hover-emphasis" data-sfx-click="sweep" class="grid grid-cols-[8.75rem_minmax(0,1fr)] gap-3 p-3 text-slate-100 transition hover:bg-cyan-400/5">
<div class="relative overflow-hidden rounded-[1rem] border border-slate-700/70 bg-slate-900">
{featuredPrimaryVisual ? (
{featuredListingVisual ? (
<Fragment>
<img
alt={featuredProduct.data.name}
class="aspect-square h-full w-full object-cover"
data-src={featuredPrimaryVisual}
data-src={featuredListingVisual.src}
data-srcset={featuredListingVisual.srcset}
data-sizes="8.75rem"
data-state="pending"
data-store-product-image
decoding="async"
Expand All @@ -79,7 +82,12 @@ const companionCount = allProducts.filter(
<span>Loading asset</span>
</div>
<noscript>
<img src={featuredPrimaryVisual} alt={featuredProduct.data.name} />
<img
src={featuredListingVisual.src}
srcset={featuredListingVisual.srcset}
sizes="8.75rem"
alt={featuredProduct.data.name}
/>
</noscript>
</Fragment>
) : (
Expand Down Expand Up @@ -261,21 +269,23 @@ const companionCount = allProducts.filter(
</div>
</div>

{spotlightProducts.length > 0 && (
{spotlightListings.length > 0 && (
<section class="storefront-spotlight">
<div class="storefront-panel__header">
<p>Sponsored Shelf</p>
<h2>Secondary Signals</h2>
</div>
<div class="storefront-spotlight__grid">
{spotlightProducts.map((product) => (
{spotlightListings.map(({ product, visual }) => (
<a href={getProductHref(product)} data-sfx-hover="hover-soft" data-sfx-click="soft" class="storefront-spotlight__card">
<div class="storefront-spotlight__media">
{getPrimaryProductVisual(product) ? (
{visual ? (
<Fragment>
<img
alt={product.data.name}
data-src={getPrimaryProductVisual(product)}
data-src={visual.src}
data-srcset={visual.srcset}
data-sizes="(max-width: 820px) calc(100vw - 6.5rem), 7rem"
data-state="pending"
data-store-product-image
decoding="async"
Expand All @@ -286,7 +296,9 @@ const companionCount = allProducts.filter(
</div>
<noscript>
<img
src={getPrimaryProductVisual(product)}
src={visual.src}
srcset={visual.srcset}
sizes="(max-width: 820px) calc(100vw - 6.5rem), 7rem"
alt={product.data.name}
loading="lazy"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
}

@media (max-width: 960px) {
.featured-product-shell {
grid-template-columns: minmax(0, 1fr);
}

.featured-product-utilitybar,
.featured-product-topbar,
.featured-product-panel__header,
Expand Down
2 changes: 1 addition & 1 deletion apps/megameal/src/styles/routeStyleBundles.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ const bundles: Record<RouteStyleBundleName, RouteStyleBundle> = {
sources: [
appStyle('features/store/featured-product/featured-product-commerce.css'),
appStyle('features/store/featured-product/featured-product-panels.css'),
appStyle('features/store/featured-product/featured-product-related-modal.css'),
appStyle('features/store/featured-product/featured-product-shell.css'),
appStyle('features/store/featured-product/featured-product-related-modal.css'),
appStyle('features/store/featured-product/procedural-model-viewer.css'),
],
},
Expand Down
Loading