mirror of
https://github.com/Xevion/the-office.git
synced 2026-01-31 04:26:17 -06:00
feat: slugify, switch episode route to title-based url
This commit is contained in:
@@ -8,31 +8,54 @@ import {
|
||||
} from '@/components/ui/accordion';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useStore from '@/store';
|
||||
|
||||
const store = useStore();
|
||||
store.preloadEpisodes();
|
||||
const seasons = computed(() => store.quoteData);
|
||||
import episodes from '@/../public/json/episodes.json';
|
||||
|
||||
const data = computed(() => {
|
||||
return episodes.map((season, seasonIndex) => {
|
||||
return {
|
||||
seasonNumber: seasonIndex + 1,
|
||||
episodes: season.map((episode, episodeIndex) => {
|
||||
const title = episode?.title;
|
||||
if (!title) {
|
||||
// throw new Error(
|
||||
// `Episode title is required for season ${seasonIndex + 1} episode ${episodeIndex + 1}`,
|
||||
// );
|
||||
return {
|
||||
episodeNumber: episodeIndex + 1,
|
||||
seasonNumber: seasonIndex + 1,
|
||||
title: 'IDK',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
episodeNumber: episodeIndex + 1,
|
||||
seasonNumber: seasonIndex + 1,
|
||||
title,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Accordion type="single" class="font-roboto-slab ml-1 w-[300px]" collapsible>
|
||||
<AccordionItem
|
||||
v-for="season in store.quoteData"
|
||||
:key="season.season_id"
|
||||
:value="`season-${season.season_id}`"
|
||||
v-for="season in data"
|
||||
:key="season.seasonNumber"
|
||||
:value="`season-${season.seasonNumber}`"
|
||||
class="group"
|
||||
>
|
||||
<AccordionTrigger
|
||||
:class="
|
||||
cn(
|
||||
'text-foreground/80 cursor-pointer border border-t-transparent bg-white/90 pr-6 pl-5 text-xl group-hover:border-zinc-400 hover:no-underline',
|
||||
season.season_id !== 9 ? 'border-b-transparent' : 'border-b',
|
||||
season.seasonNumber !== 9 ? 'border-b-transparent' : 'border-b',
|
||||
)
|
||||
"
|
||||
>
|
||||
Season {{ season.season_id }}
|
||||
Season {{ season.seasonNumber }}
|
||||
<template #icon>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
@@ -43,17 +66,14 @@ const seasons = computed(() => store.quoteData);
|
||||
<AccordionContent
|
||||
class="text-foreground/80 border-t border-r pb-0 group-hover:border-t-transparent"
|
||||
>
|
||||
<template v-for="(episode, index) in seasons[season.season_id - 1].episodes">
|
||||
<template v-if="'title' in episode">
|
||||
<SeasonListItem
|
||||
:key="`rl-${index}`"
|
||||
v-for="episode in season.episodes"
|
||||
:key="`episode-${episode.title}`"
|
||||
class="bg-white/90 hover:bg-gray-100"
|
||||
:episode-number="episode.episodeNumber"
|
||||
:season-number="episode.seasonNumber"
|
||||
:title="episode.title"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { slugify } from '@/lib/utils';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useStore from '@/store';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps<
|
||||
{
|
||||
@@ -14,20 +10,20 @@ const props = defineProps<
|
||||
} & { class?: HTMLAttributes['class'] }
|
||||
>();
|
||||
|
||||
const timeoutID = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
// const timeoutID = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const startHover = () => {
|
||||
timeoutID.value = setTimeout(() => {
|
||||
store.fetchEpisode({ season: props.seasonNumber, episode: props.episodeNumber });
|
||||
}, 500);
|
||||
};
|
||||
// const startHover = () => {
|
||||
// timeoutID.value = setTimeout(() => {
|
||||
// store.fetchEpisode({ season: props.seasonNumber, episode: props.episodeNumber });
|
||||
// }, 500);
|
||||
// };
|
||||
|
||||
const stopHover = () => {
|
||||
if (timeoutID.value !== null) {
|
||||
clearTimeout(timeoutID.value);
|
||||
timeoutID.value = null;
|
||||
}
|
||||
};
|
||||
// const stopHover = () => {
|
||||
// if (timeoutID.value !== null) {
|
||||
// clearTimeout(timeoutID.value);
|
||||
// timeoutID.value = null;
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,15 +31,13 @@ const stopHover = () => {
|
||||
:id="`s-${seasonNumber}-ep-${episodeNumber}`"
|
||||
tabindex="0"
|
||||
:aria-label="`Episode ${episodeNumber}: ${title}`"
|
||||
:to="{ name: 'Episode', params: { season: seasonNumber, episode: episodeNumber } }"
|
||||
:to="`/episode/${slugify(title)}`"
|
||||
:class="
|
||||
cn(
|
||||
'group/item focus-visible:ring-ring focus-visible:bg-accent/50 ml-2 flex py-3 pr-3 pl-4 leading-6 transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
@mouseover="startHover"
|
||||
@mouseleave="stopHover"
|
||||
>
|
||||
<span class="text-foreground/50 pr-2 select-none" :aria-hidden="true">{{
|
||||
episodeNumber.toString().padStart(2, '0')
|
||||
|
||||
+9
-1
@@ -1,6 +1,14 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { clsx } from 'clsx';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function slugify(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^\w-]+/g, '');
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import QuoteList from '@/components/features/QuoteList.vue';
|
||||
// import QuoteList from '@/components/features/QuoteList.vue';
|
||||
// import CharacterBadges from '@/components/features/CharacterBadges.vue';
|
||||
import Skeleton from '@/components/common/Skeleton.vue';
|
||||
// import Skeleton from '@/components/common/Skeleton.vue';
|
||||
import Breadcrumb from '@/components/common/Breadcrumb.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const route = useRoute();
|
||||
import episodes from '@/../public/json/episodes.json';
|
||||
import { slugify } from '@/lib/utils';
|
||||
|
||||
const params = { season: 1, episode: route.params.id };
|
||||
const { params } = useRoute();
|
||||
const episodeSlug = params.id;
|
||||
|
||||
const episode = computed(() => {
|
||||
return (
|
||||
episodes
|
||||
.flatMap((season) => season.map((episode) => episode))
|
||||
.find((episode) => slugify(episode?.title ?? '') === episodeSlug) ?? null
|
||||
);
|
||||
});
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
return [
|
||||
{ text: 'Home', to: '/' },
|
||||
{ text: `Season ${params.season}`, to: `/season/${params.season}` },
|
||||
{ text: `Season ${episode.value?.seasonNumber}`, to: `/season/${episode.value?.seasonNumber}` },
|
||||
{
|
||||
text: `Episode ${params.episode}`,
|
||||
to: `/episode/${params.episode}`,
|
||||
text: `Episode ${episode.value?.episodeNumber}`,
|
||||
to: `/episode/${episode.value?.episodeNumber}`,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user