feat!: switch to Nuxt, complete overhaul

This commit is contained in:
2025-07-16 12:47:33 -05:00
parent f5ec1d2264
commit 00c0770388
49 changed files with 7414 additions and 1673 deletions
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="outer-footer">
<footer class="inner-footer">
<BContainer>
<BRow style="text-align: center">
<ul>
<li>
<a href="https://github.com/Xevion/the-office">GitHub</a>
</li>
<li>
<a :href="latestCommitUrl">Latest Commit</a>
</li>
<li>
<a href="https://github.com/Xevion/the-office/issues/new">Report Issues</a>
</li>
<li>
<a href="https://xevion.dev">Xevion.dev</a>
</li>
</ul>
</BRow>
<p v-if="buildTimeString !== null" class="build-time" :title="buildISOString">
built on {{ buildTimeString }}
</p>
</BContainer>
</footer>
</div>
</template>
<script lang="ts">
import { BContainer, BRow } from 'bootstrap-vue-next'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'FooterComponent',
components: {
BContainer,
BRow,
},
props: {
buildMoment: { type: Object, default: null },
},
computed: {
buildTimeString() {
return this.buildMoment.format('MMM Do, YYYY [at] h:mm A zz')
},
buildISOString() {
return this.buildMoment.toISOString()
},
latestCommitUrl() {
return `https://github.com/Xevion/the-office/commit/${import.meta.env.VUE_APP_GIT_HASH}`
},
},
})
</script>
<style lang="scss" scoped>
.outer-footer {
height: 100px;
width: 100%;
position: absolute;
left: 0;
bottom: 0;
}
.inner-footer {
margin: 0 auto;
height: 100%;
color: #6d6d6d;
.build-time {
text-align: center;
padding-top: 0.7em;
opacity: 0.3;
font-size: 0.85em;
margin-bottom: 0;
}
ul {
padding: 0;
list-style: none;
line-height: 1.6;
font-size: 14px;
display: table;
margin: 0 auto;
li {
&:not(:last-child)::after {
padding: 0 0.6em;
content: '|';
}
display: inline;
}
a {
color: inherit;
text-decoration: none;
opacity: 0.6;
&:hover {
opacity: 0.8;
}
}
}
}
</style>
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { ref } from 'vue';
import { SearchIcon } from 'lucide-vue-next';
const searchQuery = ref('');
</script>
<template>
<div class="relative">
<input
v-model="searchQuery"
type="text"
placeholder="Quotes, characters, episodes..."
class="w-72 rounded-lg border border-gray-400 py-2 pr-4 pl-10 outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<SearchIcon class="size-5 text-gray-500" />
</div>
</div>
</template>
+61
View File
@@ -0,0 +1,61 @@
<script setup lang="ts">
import SeasonListItem from '@/components/layout/SeasonListItem.vue';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} 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);
</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}`"
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 {{ season.season_id }}
<template #icon>
<ChevronDown
aria-hidden="true"
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-1.5 transition-transform duration-200"
/>
</template>
</AccordionTrigger>
<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
class="bg-white/90 hover:bg-gray-100"
:key="`rl-${index}`"
:episode-number="episode.episodeNumber"
:season-number="episode.seasonNumber"
:title="episode.title"
/>
</template>
</template>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
+58
View File
@@ -0,0 +1,58 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { RouterLink } from 'vue-router';
import { cn } from '@/lib/utils';
import useStore from '@/store';
import { ref } from 'vue';
const store = useStore();
const props = defineProps<
{
episodeNumber: number;
seasonNumber: number;
title: string;
} & { class?: HTMLAttributes['class'] }
>();
const timeoutID = ref<number | null>(null);
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;
}
};
</script>
<template>
<RouterLink
tabindex="0"
:aria-label="`Episode ${episodeNumber}: ${title}`"
:id="`s-${seasonNumber}-ep-${episodeNumber}`"
:to="{ name: 'Episode', params: { season: seasonNumber, episode: episodeNumber } }"
: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')
}}</span>
<span class="text-foreground/80 group-hover/item:text-black">
&OpenCurlyDoubleQuote;{{ title }}&CloseCurlyDoubleQuote;
</span>
</RouterLink>
</template>
<style scoped></style>