Files
vmall/apps/mall/pages/index.vue
T

155 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { Category, HomeContent, Product } from "@vmall/shared";
import { t as pick } from "@vmall/shared";
interface HomeFloor {
categoryId: string;
name: Record<string, string>;
advImage: string | null;
advUrl: string;
products: Product[];
}
const emptyContent: HomeContent = { banners: [], promos: [], quick_links: [], floor_adverts: [] };
const { locale, t } = useI18n();
const { $api } = useNuxtApp();
// Shared with the shell's category menu, so this does not refetch the tree.
const { data: categories } = await useAsyncData("shell-categories", () => $api.listCategories());
// Marketing content comes from the content API; an outage leaves these lists
// empty rather than breaking the page.
const { data: content } = await useAsyncData("home-content", () => $api.getHomeContent(), {
default: () => emptyContent,
});
const banners = computed(() =>
content.value.banners.map((banner) => ({ image: banner.image, url: banner.url })),
);
const quickLinks = computed(() => content.value.quick_links);
const promos = computed(() => content.value.promos);
const heroQuickLinks = computed(() => quickLinks.value.slice(0, 6));
const sidePromos = computed(() => promos.value.slice(0, 2));
const stripPromos = computed(() =>
promos.value.length > 2 ? promos.value.slice(2, 5) : promos.value.slice(0, 3),
);
// Floor structure comes from the category tree and floor products from the
// catalog API; advert art is an ordered pool assigned to floors in turn.
const { data: floors } = await useAsyncData("home-floors", async () => {
const roots: Category[] = categories.value ?? (await $api.listCategories());
const adverts = content.value.floor_adverts;
const tops = roots
.filter((category) => category.parent_id === null)
.sort((a, b) => a.position - b.position);
const built = await Promise.all(
tops.map(async (category, index) => {
const page = await $api.listProducts({ category_id: category.id, per_page: 8 });
const floor: HomeFloor = {
categoryId: category.id,
name: category.name,
advImage: adverts.length > 0 ? adverts[index % adverts.length].image : null,
advUrl: `/search?category=${category.id}`,
products: page.items,
};
return floor;
}),
);
return built.filter((floor) => floor.products.length > 0);
});
const visibleFloors = computed(() => floors.value ?? []);
</script>
<template>
<div>
<section class="max-w-mall mx-auto px-4">
<div class="h-hero bg-surface grid grid-cols-[240px_minmax(0,1fr)_220px]">
<ShellCategoryMenu pinned />
<div class="min-w-0">
<UiCarousel :images="banners" :height="450" />
</div>
<aside class="border-border bg-surface flex h-full flex-col border-l">
<ul class="bg-border grid list-none grid-cols-2 gap-px p-px">
<li v-for="q in heroQuickLinks" :key="q.id" class="bg-surface text-center">
<NuxtLink
:to="q.url"
class="text-muted hover:text-primary block px-1 py-3 text-[12px] no-underline transition-colors"
>
<svg
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
class="mx-auto mb-1.5"
>
<path :d="q.glyph" />
</svg>
<span>{{ pick(q.label, locale) }}</span>
</NuxtLink>
</li>
</ul>
<div class="mt-2 flex-1 space-y-2 px-2 pb-2">
<NuxtLink v-for="p in sidePromos" :key="p.id" :to="p.url" class="block overflow-hidden">
<img
:src="p.image"
:alt="t('home.hotPromo')"
loading="lazy"
class="block h-[106px] w-full object-cover"
/>
</NuxtLink>
</div>
</aside>
</div>
</section>
<section v-if="stripPromos.length > 0" class="max-w-mall mx-auto mt-5 mb-5 px-4">
<div class="grid grid-cols-3 gap-3">
<NuxtLink v-for="p in stripPromos" :key="p.id" :to="p.url" class="block overflow-hidden">
<img
:src="p.image"
:alt="t('home.hotPromo')"
loading="lazy"
class="block h-40 w-full object-cover"
/>
</NuxtLink>
</div>
</section>
<div class="bg-bg py-8">
<section
v-for="floor in visibleFloors"
:key="floor.categoryId"
class="max-w-mall mx-auto mb-[30px] px-4 last:mb-0"
>
<header class="border-primary flex items-end justify-between border-b-2">
<h2 class="m-0 py-[10px] text-[18px] leading-none">{{ pick(floor.name, locale) }}</h2>
<NuxtLink
:to="`/search?category=${floor.categoryId}`"
class="text-muted hover:text-primary pb-2 text-[12px] no-underline transition-colors"
>{{ t("home.viewMore") }} ›</NuxtLink
>
</header>
<div class="bg-surface flex">
<NuxtLink
v-if="floor.advImage"
:to="floor.advUrl"
class="border-border shrink-0 border-r"
>
<img
:src="floor.advImage"
:alt="pick(floor.name, locale)"
loading="lazy"
class="block h-[614px] w-[234px] object-cover"
/>
</NuxtLink>
<div class="bg-border grid min-w-0 flex-1 grid-cols-4 gap-px p-px">
<UiProductCard v-for="p in floor.products" :key="p.id" :product="p" />
</div>
</div>
</section>
</div>
</div>
</template>