Done
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import ArticleCard from './ArticleCard.vue';
|
||||
|
||||
interface Story {
|
||||
source: string;
|
||||
time: string | null;
|
||||
headline: string;
|
||||
deck?: string | null;
|
||||
image?: string | null;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
label: string;
|
||||
hero: boolean;
|
||||
items: Story[];
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
category: Category;
|
||||
index?: number;
|
||||
}>(),
|
||||
{
|
||||
index: 0,
|
||||
},
|
||||
);
|
||||
|
||||
type Variant = 'hero' | 'banner' | 'feature' | 'standard' | 'brief';
|
||||
|
||||
interface Slot {
|
||||
variant: Variant;
|
||||
span: number;
|
||||
}
|
||||
|
||||
// A hero category always gets the same big-lead treatment. Non-hero
|
||||
// categories rotate through a few distinct shapes by position, so
|
||||
// adjacent sections don't all read as the same grid: one leans on two
|
||||
// medium photos, one leans on a single wide banner, one is mostly
|
||||
// small thumbnails with more falling through to the text-only list.
|
||||
const nonHeroLayouts: Slot[][] = [
|
||||
// gallery: two medium features side by side, three small below
|
||||
[
|
||||
{ variant: 'feature', span: 3 },
|
||||
{ variant: 'feature', span: 3 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
],
|
||||
// lead: one wide banner feature, three small below
|
||||
[
|
||||
{ variant: 'banner', span: 6 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
],
|
||||
// digest: small thumbnails only, text-heavy section
|
||||
[
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
],
|
||||
];
|
||||
|
||||
const heroLayout: Slot[] = [
|
||||
{ variant: 'hero', span: 6 },
|
||||
{ variant: 'feature', span: 3 },
|
||||
{ variant: 'feature', span: 3 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
{ variant: 'standard', span: 2 },
|
||||
];
|
||||
|
||||
const slots = props.category.hero
|
||||
? heroLayout
|
||||
: nonHeroLayouts[props.index % nonHeroLayouts.length];
|
||||
|
||||
const gridItems = props.category.items
|
||||
.slice(0, slots.length)
|
||||
.map((story, i) => ({
|
||||
story,
|
||||
variant: slots[i].variant,
|
||||
span: slots[i].span,
|
||||
}));
|
||||
|
||||
const briefItems = props.category.items.slice(slots.length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="category">
|
||||
<div class="category-header">
|
||||
<span class="category-label">{{ category.label }}</span>
|
||||
<a href="#" class="category-more">More</a>
|
||||
</div>
|
||||
|
||||
<div v-if="category.items.length === 0" class="category-empty">
|
||||
No stories yet. Feeds may still be fetching.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="section-grid">
|
||||
<ArticleCard
|
||||
v-for="entry in gridItems"
|
||||
:key="entry.story.link"
|
||||
:story="entry.story"
|
||||
:variant="entry.variant"
|
||||
:style="{ gridColumn: `span ${entry.span}` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="briefItems.length" class="brief-section">
|
||||
<div class="brief-label">In brief</div>
|
||||
<div class="brief-grid">
|
||||
<ArticleCard
|
||||
v-for="item in briefItems"
|
||||
:key="item.link"
|
||||
:story="item"
|
||||
variant="brief"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.category
|
||||
border-bottom: 1px solid #E8E8E8
|
||||
padding: 1.5rem 0
|
||||
|
||||
&:last-child
|
||||
border-bottom: none
|
||||
|
||||
.category-header
|
||||
display: flex
|
||||
align-items: baseline
|
||||
justify-content: space-between
|
||||
margin-bottom: 1rem
|
||||
|
||||
.category-label
|
||||
font-size: 10px
|
||||
font-weight: 600
|
||||
letter-spacing: 2px
|
||||
text-transform: uppercase
|
||||
color: #111
|
||||
|
||||
.category-more
|
||||
font-size: 12px
|
||||
color: #C0392B
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
text-decoration: underline
|
||||
|
||||
.category-empty
|
||||
font-size: 12px
|
||||
color: #999
|
||||
padding: 1rem 0
|
||||
|
||||
.section-grid
|
||||
display: grid
|
||||
grid-template-columns: repeat(6, 1fr)
|
||||
gap: 1px
|
||||
background: #E8E8E8
|
||||
|
||||
.brief-section
|
||||
margin-top: 4px
|
||||
|
||||
.brief-label
|
||||
font-size: 10px
|
||||
font-weight: 600
|
||||
letter-spacing: 1px
|
||||
text-transform: uppercase
|
||||
color: #999
|
||||
padding: 0.9rem 0 0.3rem
|
||||
|
||||
.brief-grid
|
||||
display: grid
|
||||
grid-template-columns: repeat(2, 1fr)
|
||||
column-gap: 2rem
|
||||
</style>
|
||||
Reference in New Issue
Block a user