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
Vendored
+19 -28
View File
@@ -1,35 +1,26 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs # Logs
logs logs
*.log *.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules # Misc
.DS_Store .DS_Store
dist .fleet
dist-ssr .idea
coverage
*.local
/cypress/videos/ # Local env files
/cypress/screenshots/ .env
.env.*
!.env.example
# Editor directories and files build/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Repository specific
*.scss.css
*.scss.css.map
build
*.cache
# .env files
.env
+1 -1
View File
@@ -11,7 +11,7 @@ const toggleSidebar = () => {
}; };
const headings = [ const headings = [
{ name: 'Home', href: '/' }, { href: '/' },
{ name: 'Episodes', href: '/episodes' }, { name: 'Episodes', href: '/episodes' },
{ name: 'Characters', href: '/characters' }, { name: 'Characters', href: '/characters' },
{ name: 'Seasons', href: '/seasons' }, { name: 'Seasons', href: '/seasons' },

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+2 -2
View File
@@ -46,8 +46,6 @@
} }
:root { :root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
@@ -79,6 +77,8 @@
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
} }
.dark { .dark {
@@ -9,14 +9,12 @@ import {
import type { HTMLAttributes } from 'vue'; import type { HTMLAttributes } from 'vue';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { computed } from 'vue'; import { computed } from 'vue';
import.meta.glob('/public/json/*.json');
console.log();
const lastIndex = computed(() => props.items.length - 1); const lastIndex = computed(() => props.items.length - 1);
const props = defineProps< const props = defineProps<
{ {
items: { text: string; to: { name: string; params?: Record<string, string> } }[]; items: { text: string; to?: string }[];
} & { class?: HTMLAttributes['class'] } } & { class?: HTMLAttributes['class'] }
>(); >();
</script> </script>
@@ -27,10 +25,10 @@ const props = defineProps<
<template v-for="(item, index) in items" :key="item.text"> <template v-for="(item, index) in items" :key="item.text">
<BreadcrumbSeparator v-if="index !== 0" /> <BreadcrumbSeparator v-if="index !== 0" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink class="text-gray-600" :href="item.to.name" as-child> <BreadcrumbLink class="text-gray-600" as-child>
<RouterLink :to="item.to" v-if="index !== lastIndex"> <NuxtLink :to="item.to" v-if="index !== lastIndex">
{{ item.text }} {{ item.text }}
</RouterLink> </NuxtLink>
<span v-else>{{ item.text }}</span> <span v-else>{{ item.text }}</span>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+10 -2
View File
@@ -1,4 +1,4 @@
<template> <!-- <template>
<div> <div>
<BBreadcrumb :items="breadcrumbs" /> <BBreadcrumb :items="breadcrumbs" />
<BCard> <BCard>
@@ -52,4 +52,12 @@ export default defineComponent({
}) })
</script> </script>
<style scoped></style> <style scoped></style> -->
<script setup lang="ts"></script>
<template>
<div>
<h1>About</h1>
</div>
</template>
@@ -1,4 +1,4 @@
<template> <!-- <template>
<div> <div>
<BBreadcrumb v-if="ready" :items="breadcrumbs" /> <BBreadcrumb v-if="ready" :items="breadcrumbs" />
<BCard v-else class="breadcrumb-skeleton mb-3"> <BCard v-else class="breadcrumb-skeleton mb-3">
@@ -104,4 +104,12 @@ export default defineComponent({
}, },
}, },
}); });
</script> </script> -->
<script setup lang="ts"></script>
<template>
<div>
<h1>Character</h1>
</div>
</template>
@@ -1,4 +1,4 @@
<template> <!-- <template>
<div> <div>
<template v-if="ready"> <template v-if="ready">
<BBreadcrumb v-if="ready" :items="breadcrumbs" /> <BBreadcrumb v-if="ready" :items="breadcrumbs" />
@@ -25,7 +25,7 @@
class="no-link" class="no-link"
:to="{ name: 'Character', params: { character: id } }" :to="{ name: 'Character', params: { character: id } }"
> >
<!-- <b-icon class="h6" icon="caret-right-fill" /> --> <b-icon class="h6" icon="caret-right-fill" />
</RouterLink> </RouterLink>
<span class="h6 font-italic" style="opacity: 50%"> <span class="h6 font-italic" style="opacity: 50%">
{{ characters[id].actor }} {{ characters[id].actor }}
@@ -135,3 +135,12 @@ export default defineComponent({
}, },
}); });
</script> </script>
-->
<script setup lang="ts"></script>
<template>
<div>
<h1>About</h1>
</div>
</template>
@@ -1,27 +1,21 @@
<script setup lang="ts"> <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 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 Breadcrumb from '@/components/common/Breadcrumb.vue';
import { computed } from 'vue'; import { computed } from 'vue';
// import { useRoute } from 'vue-router';
// const route = useRoute(); const route = useRoute();
const route = { const params = { season: 1, episode: route.params.id };
params: {
season: '1',
episode: '1',
},
};
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
return [ return [
{ text: 'Home', to: { name: 'Home' } }, { text: 'Home', to: '/' },
{ text: `Season ${route.params.season}`, to: { name: 'Season', season: route.params.season } }, { text: `Season ${params.season}`, to: `/season/${params.season}` },
{ {
text: `Episode ${route.params.episode}`, text: `Episode ${params.episode}`,
to: { name: 'Episode', season: route.params.season, episode: route.params.episode }, to: `/episode/${params.episode}`,
}, },
]; ];
}); });
@@ -117,7 +111,7 @@ const breadcrumbs = computed(() => {
<!-- <BCard v-else class="breadcrumb-skeleton mb-3"> <!-- <BCard v-else class="breadcrumb-skeleton mb-3">
<Skeleton style="width: 40%" /> <Skeleton style="width: 40%" />
</BCard> --> </BCard> -->
<BCard class="mb-4"> <!-- <BCard class="mb-4">
<template v-if="ready"> <template v-if="ready">
<h3 class="card-title">"{{ episode.title }}"</h3> <h3 class="card-title">"{{ episode.title }}"</h3>
<span>{{ episode.description }}</span> <span>{{ episode.description }}</span>
@@ -129,8 +123,8 @@ const breadcrumbs = computed(() => {
<Skeleton style="width: 45%; height: 60%" /> <Skeleton style="width: 45%; height: 60%" />
<Skeleton style="width: 69%; height: 40%" /> <Skeleton style="width: 69%; height: 40%" />
</template> </template>
</BCard> </BCard> -->
<div v-if="ready"> <!-- <div v-if="ready">
<BCard <BCard
v-for="(scene, sceneIndex) in episode.scenes" v-for="(scene, sceneIndex) in episode.scenes"
:key="`scene-${sceneIndex}`" :key="`scene-${sceneIndex}`"
@@ -148,7 +142,7 @@ const breadcrumbs = computed(() => {
</span> </span>
</BCardText> </BCardText>
</BCard> </BCard>
</div> </div> -->
</div> </div>
</template> </template>
+37 -40
View File
@@ -1,5 +1,5 @@
<template> <template>
<BCard> <!-- <BCard>
<template v-if="ready"> <template v-if="ready">
<h4>The Office Quotes</h4> <h4>The Office Quotes</h4>
<BCardText> <BCardText>
@@ -23,54 +23,51 @@
<Skeleton style="width: 60%" /> <Skeleton style="width: 60%" />
<Skeleton style="width: 60%" /> <Skeleton style="width: 60%" />
</BCardText> </BCardText>
</BCard> </BCard> -->
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import axios from 'axios';
import Skeleton from '@/components/common/Skeleton.vue'; import Skeleton from '@/components/common/Skeleton.vue';
import { BCardText, BCard } from 'bootstrap-vue-next';
export default defineComponent({ // export default defineComponent({
name: 'HomeComponent', // name: 'HomeComponent',
components: { // components: {
Skeleton, // Skeleton,
BCardText, // BCardText,
BCard, // BCard,
}, // },
data() { // data() {
return { // return {
stats: null, // stats: null,
}; // };
}, // },
computed: { // computed: {
ready() { // ready() {
return true; // return true;
// return this.stats != null; // // return this.stats != null;
}, // },
}, // },
created() { // created() {
// this.getStats(); // // this.getStats();
}, // },
methods: { // methods: {
getStats() { // getStats() {
const path = `${import.meta.env.VUE_APP_API_URL}/api/stats/`; // const path = `${import.meta.env.VUE_APP_API_URL}/api/stats/`;
axios // axios
.get(path) // .get(path)
.then((res) => { // .then((res) => {
this.stats = res.data; // this.stats = res.data;
}) // })
.catch((error) => { // .catch((error) => {
console.error(error); // console.error(error);
}); // });
}, // },
}, // },
}); // });
</script> </script>
@@ -1,4 +1,4 @@
<template> <!-- <template>
<div> <div>
<BBreadcrumb :items="breadcrumbs" /> <BBreadcrumb :items="breadcrumbs" />
<BCard v-if="ready"> <BCard v-if="ready">
@@ -102,4 +102,12 @@ export default defineComponent({
}, },
}, },
}) })
</script> </script> -->
<script setup lang="ts"></script>
<template>
<div>
<h1>About</h1>
</div>
</template>
+31 -32
View File
@@ -1,5 +1,4 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import axios from 'axios';
export interface Character { export interface Character {
name: string; name: string;
@@ -127,49 +126,49 @@ const useStore = defineStore('main', {
} }
const path = `/json/${payload.season.toString().padStart(2, '0')}/${payload.episode.toString().padStart(2, '0')}.json`; const path = `/json/${payload.season.toString().padStart(2, '0')}/${payload.episode.toString().padStart(2, '0')}.json`;
axios // axios
.get(path) // .get(path)
.then((res) => { // .then((res) => {
// Push episode data // // Push episode data
this.mergeEpisode({ // this.mergeEpisode({
season: payload.season, // season: payload.season,
episode: payload.episode, // episode: payload.episode,
episodeData: res.data, // episodeData: res.data,
}); // });
resolve(); // resolve();
}) // })
.catch((error) => { // .catch((error) => {
console.error(error); // console.error(error);
reject(error); // reject(error);
}); // });
}); });
}, },
preloadEpisodes(): void { preloadEpisodes(): void {
const path = `/json/episodes.json`; const path = `/json/episodes.json`;
axios // axios
.get(path) // .get(path)
.then((res) => { // .then((res) => {
this.mergeEpisodes(res.data as Episode[][]); // this.mergeEpisodes(res.data as Episode[][]);
this.setPreloaded({ type: 'episodes', status: true }); // this.setPreloaded({ type: 'episodes', status: true });
}) // })
.catch((error) => { // .catch((error) => {
console.error(error); // console.error(error);
}); // });
}, },
async preloadCharacters(): Promise<void> { async preloadCharacters(): Promise<void> {
if (this.checkPreloaded('characters')) return; if (this.checkPreloaded('characters')) return;
const path = `/json/characters.json`; const path = `/json/characters.json`;
let res = null; let res = null;
try { // try {
res = await axios.get(path); // res = await axios.get(path);
} catch (error) { // } catch (error) {
console.error(error); // console.error(error);
throw error; // throw error;
} // }
this.mergeCharacters({ characters: res.data }); // this.mergeCharacters({ characters: res.data });
this.setPreloaded({ type: 'characters', status: true }); this.setPreloaded({ type: 'characters', status: true });
}, },
}, },
+1 -1
View File
@@ -4,7 +4,7 @@
"typescript": true, "typescript": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "src/index.css", "css": "app/assets/tailwind.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
+36
View File
@@ -0,0 +1,36 @@
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
css: ['~/assets/tailwind.css', '@fontsource-variable/roboto-slab', '@fontsource/open-sans'],
vite: {
plugins: [tailwindcss()],
},
modules: [
'@nuxt/test-utils',
'@nuxt/ui',
'@nuxt/eslint',
'@nuxt/image',
'shadcn-nuxt',
'@pinia/nuxt'
],
shadcn: {
prefix: '',
componentDir: './app/components/ui',
},
typescript: {
typeCheck: true,
tsConfig: {
compilerOptions: {
allowSyntheticDefaultImports: true,
allowArbitraryExtensions: true,
baseUrl: '.',
paths: {
'@/*': ['./src/*'],
},
},
}
}
})
+18 -57
View File
@@ -4,77 +4,38 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "build": "nuxt build",
"build": "run-p type-check \"build-only {@}\" --", "dev": "nuxt dev",
"preview": "vite preview", "generate": "nuxt generate",
"test:unit": "vitest", "preview": "nuxt preview",
"build-only": "vite build", "postinstall": "nuxt prepare"
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/roboto-slab": "^5.2.6", "@fontsource-variable/roboto-slab": "^5.2.6",
"@fontsource/open-sans": "^5.2.6", "@fontsource/open-sans": "^5.2.6",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@nuxt/eslint": "1.5.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@nuxt/image": "1.10.0",
"@fortawesome/vue-fontawesome": "^3.0.8", "@nuxt/test-utils": "3.19.2",
"@nuxt/ui": "3.2.0",
"@pinia/nuxt": "0.11.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0",
"algoliasearch": "^5.32.0",
"axios": "^1.10.0",
"bootstrap": "^5.3.7",
"bootstrap-vue-next": "^0.30.4",
"browserslist": "^4.25.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"ejs": "^3.1.10", "eslint": "^9.0.0",
"instantsearch.css": "^8.5.1",
"lucide-vue-next": "^0.525.0", "lucide-vue-next": "^0.525.0",
"moment": "^2.30.1", "nuxt": "^4.0.0",
"node-forge": "1.3.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"postcss": "^8",
"reka-ui": "^2.3.2", "reka-ui": "^2.3.2",
"sass": "^1.89.2", "shadcn-nuxt": "2.2.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"tw-animate-css": "^1.3.5", "tw-animate-css": "^1.3.5",
"vue": "^3.5.17", "vue": "^3.5.17",
"vue-autosuggest": "^2.2.0", "vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"ws": "^6.2.3"
}, },
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf",
"devDependencies": { "devDependencies": {
"@iconify-json/radix-icons": "^1.2.2", "typescript": "^5.8.3",
"@iconify/vue": "^5.0.0", "vue-tsc": "^3.0.1"
"@tsconfig/node22": "^22.0.2", }
"@types/jsdom": "^21.1.7",
"@types/node": "^24.0.14",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vitest/eslint-plugin": "^1.3.4",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "~10.2.0",
"jiti": "^2.4.2",
"jsdom": "^26.1.0",
"npm-run-all2": "^8.0.4",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "^0.6.14",
"typescript": "~5.8.3",
"vite": "^7.0.4",
"vite-plugin-vue-devtools": "^7.7.7",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.12"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf"
} }
+7197 -1367
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
User-Agent: *
Disallow:
-26
View File
@@ -1,26 +0,0 @@
import '@fontsource/open-sans';
import '@fontsource-variable/roboto-slab';
import './index.css';
import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router.ts';
import { createPinia } from 'pinia';
// import { createBootstrap, vBToggle } from 'bootstrap-vue-next';
// Add the necessary CSS
// import 'bootstrap/dist/css/bootstrap.css';
// import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
// Prevent invalid episodes, seasons and characters from being accessed
router.beforeEach((to, from, next) => {
if (from.name !== null && to.name === 'Character' && false) {
} else next();
});
const pinia = createPinia();
const app = createApp(App);
app.use(router);
app.use(pinia);
app.mount('#app');
-66
View File
@@ -1,66 +0,0 @@
import Home from '@/views/Home.vue';
import Episode from '@/views/Episode.vue';
import SearchResults from '@/views/SearchResults.vue';
import Character from '@/views/Character.vue';
import Season from '@/views/Season.vue';
import Characters from '@/views/Characters.vue';
import About from '@/views/About.vue';
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about/',
name: 'About',
component: About,
},
{
path: '/characters/',
name: 'Characters',
component: Characters,
},
{
path: '/search_results',
name: 'SearchResults',
component: SearchResults,
},
{
path: '/character/:character',
name: 'Character',
component: Character,
},
{
path: '/season/:season',
name: 'Season',
component: Season,
},
{
path: '/episode/:episode',
name: 'Episode',
component: Episode,
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', redirect: '/' }, // catch all
],
scrollBehavior(to, from, savedPosition) {
// https://router.vuejs.org/guide/advanced/scroll-behavior.html
if (to.hash) {
return { el: to.hash, behavior: 'smooth' };
}
if (savedPosition) {
return savedPosition;
}
return {
x: 0,
y: 0,
};
},
});
export default router;
+8 -4
View File
@@ -1,20 +1,24 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript
"files": [], "files": [],
"references": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./.nuxt/tsconfig.app.json"
}, },
{ {
"path": "./tsconfig.app.json" "path": "./.nuxt/tsconfig.server.json"
}, },
{ {
"path": "./tsconfig.vitest.json" "path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
} }
], ],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./app/*"]
} }
} }
} }
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueDevTools from 'vite-plugin-vue-devtools';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
outDir: './build',
},
});