mirror of
https://github.com/Xevion/the-office.git
synced 2026-01-31 08:26:13 -06:00
feat!: switch to Nuxt, complete overhaul
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
“{{ title }}”
|
||||
</span>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user