mirror of
https://github.com/Xevion/the-office.git
synced 2026-01-31 10:26:21 -06:00
feat!: upgrade to Vue3, TypeScript, TailwindCSS, Pinia
- Added tailwindcss, shadcn, vite, typescript, pinia - Removed webpack, ejs, moment, instantsearch, algolia, bootstrap - Disabled most sass, original components - Began redesigning light themed index page
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
@@ -1,30 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
parser: "babel-eslint",
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/essential',
|
||||
'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
|
||||
],
|
||||
rules: {
|
||||
// override/add rules settings here, such as:
|
||||
'vue/no-unused-vars': 'error',
|
||||
'indent': ['warn', 4],
|
||||
"vue/html-indent": ["error", 4, {
|
||||
"attribute": 1,
|
||||
"baseIndent": 1,
|
||||
"closeBracket": 0,
|
||||
"alignAttributesVertically": true,
|
||||
"ignores": []
|
||||
}],
|
||||
'vue/max-attributes-per-line': ["warn", {
|
||||
"singleline": {"max": 5},
|
||||
"multiline": {"max": 2}
|
||||
}]
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -1,3 +1,5 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
data/normalization/html/* linguist-vendored
|
||||
public/json/** linguist-generated
|
||||
public/img/** linguist-generated
|
||||
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
VUE_APP_ALGOLIA_APP_ID: '${{ secrets.ALGOLIA_APP_ID }}'
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
name: Deploy to Firebase Hosting on PR
|
||||
'on': pull_request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
VUE_APP_ALGOLIA_APP_ID: '${{ secrets.ALGOLIA_APP_ID }}'
|
||||
|
||||
Vendored
+24
-19
@@ -1,31 +1,36 @@
|
||||
# Repository specific
|
||||
.log*
|
||||
.vscode/**
|
||||
.idea/**
|
||||
*.scss.css
|
||||
*.scss.css.map
|
||||
node_modules/**
|
||||
/build
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.cache
|
||||
|
||||
# .env files
|
||||
.env
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.tsbuildinfo
|
||||
|
||||
# Repository specific
|
||||
*.scss.css
|
||||
*.scss.css.map
|
||||
build
|
||||
*.cache
|
||||
my-vue-app/
|
||||
|
||||
# .env files
|
||||
.env
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts",
|
||||
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
|
||||
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginVitest from '@vitest/eslint-plugin'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
},
|
||||
skipFormatting,
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+78
-64
@@ -1,66 +1,80 @@
|
||||
{
|
||||
"name": "the-office",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.0",
|
||||
"@vue/cli-plugin-babel": "^4.4.0",
|
||||
"@vue/cli-plugin-eslint": "^4.4.0",
|
||||
"@vue/cli-service": "^4.4.0",
|
||||
"algoliasearch": "^4.3.1",
|
||||
"axios": ">=0.21.1",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-vue": "^2.16.0",
|
||||
"browserslist": "4.20.3",
|
||||
"core-js": "^3.6.5",
|
||||
"dns-packet": "1.3.4",
|
||||
"ejs": "^3.1.7",
|
||||
"file-loader": "^6.1.0",
|
||||
"follow-redirects": "^1.15.0",
|
||||
"instantsearch.css": "7.1.0",
|
||||
"minimist": "1.2.6",
|
||||
"moment": "^2.29.3",
|
||||
"node-forge": "1.3.0",
|
||||
"path-parse": "1.0.7",
|
||||
"postcss": "^7.0.39",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"url-loader": "^4.1.0",
|
||||
"url-parse": "1.5.10",
|
||||
"vue": "^2.6.11",
|
||||
"vue-autosuggest": "^2.2.0",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
"vue-instantsearch": "^3.1.0",
|
||||
"vue-loading-skeleton": "^1.1.9",
|
||||
"vue-progressive-image": "^3.2.0",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue-scrollto": "^2.18.2",
|
||||
"vue-server-renderer": "^2.6.11",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuex": "^3.5.1",
|
||||
"ws": "6.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"git-describe": "^4.1.0",
|
||||
"prettier": "^2.1.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
"name": "the-office",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/roboto-slab": "^5.2.6",
|
||||
"@fontsource/open-sans": "^5.2.6",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@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",
|
||||
"clsx": "^2.1.1",
|
||||
"ejs": "^3.1.10",
|
||||
"instantsearch.css": "^8.5.1",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"moment": "^2.30.1",
|
||||
"node-forge": "1.3.0",
|
||||
"pinia": "^3.0.3",
|
||||
"postcss": "^8",
|
||||
"reka-ui": "^2.3.2",
|
||||
"sass": "^1.89.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"vue": "^3.5.17",
|
||||
"vue-autosuggest": "^2.2.0",
|
||||
"vue-router": "^4.5.1",
|
||||
"ws": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/radix-icons": "^1.2.2",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@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"
|
||||
}
|
||||
|
||||
Generated
+5568
File diff suppressed because it is too large
Load Diff
+111
-160
@@ -1,164 +1,115 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="wrapper">
|
||||
<b-navbar>
|
||||
<b-navbar-brand>
|
||||
<router-link :to="{ name: 'Home' }" class="no-link">
|
||||
The Office
|
||||
</router-link>
|
||||
<b-badge v-if="showBreakpointMarker" style="font-size: 0.80rem;" class="mx-2" variant="dark">
|
||||
<span id="marker-xs" class="d-sm-none">XS</span>
|
||||
<span id="marker-sm" class="d-none d-sm-block d-md-none">SM</span>
|
||||
<span id="marker-md" class="d-none d-md-block d-lg-none">MD</span>
|
||||
<span id="marker-lg" class="d-none d-lg-block d-xl-none">LG</span>
|
||||
<span id="marker-xl" class="d-none d-xl-block">XL</span>
|
||||
</b-badge>
|
||||
</b-navbar-brand>
|
||||
<b-collapse id="nav-collapse" is-nav>
|
||||
<b-navbar-nav>
|
||||
<router-link :to="{ name: 'Home' }" class="nav-link no-link">
|
||||
Home
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'About' }" class="nav-link no-link">
|
||||
About
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'Characters' }" class="nav-link no-link">
|
||||
Characters
|
||||
</router-link>
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
<ais-instant-search
|
||||
index-name="prod_THEOFFICEQUOTES" :search-client="searchClient"
|
||||
:insights-client="insightsClient"
|
||||
>
|
||||
<b-container :fluid="true" class="py-2 px-lg-5 px-md-4">
|
||||
<b-row class="my-3 pl-1" cols="12">
|
||||
<b-col lg="3" xl="2" md="12">
|
||||
<ais-search-box ref="searchbox" placeholder="Search here…" @keydown.native="showResults" />
|
||||
<!--<Skeleton
|
||||
secondary-color="#3e3e3e"
|
||||
border-radius="1px"
|
||||
primary-color="#4A4A4A"
|
||||
:inner-style="{ 'min-height': '35.6px' }"
|
||||
/>-->
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row align-h="start" cols="12">
|
||||
<b-col lg="3" xl="2" md="12">
|
||||
<SeasonList />
|
||||
</b-col>
|
||||
<b-col lg="8" xl="7" md="12" class="pt-md-2 pt-lg-0">
|
||||
<router-view />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<ais-configure :analytics="true" />
|
||||
</ais-instant-search>
|
||||
<Footer :build-moment="buildMoment" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import SeasonList from '@/components/SeasonList.vue';
|
||||
import { ref } from 'vue';
|
||||
import logoSrc from '@/assets/logo.svg';
|
||||
|
||||
<style lang="scss">
|
||||
html, body, #app {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
padding-bottom: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.ais-InstantSearch {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ais-SearchBox-form {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ais-SearchBox-input {
|
||||
color: $grey-8;
|
||||
background-color: $grey-6;
|
||||
border-color: transparent;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.ais-SearchBox-submitIcon,
|
||||
.ais-SearchBox-resetIcon {
|
||||
> path {
|
||||
fill: $grey-9;
|
||||
}
|
||||
}
|
||||
|
||||
.ais-SearchBox-input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#marker-xs {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
#marker-sm {
|
||||
color: #f37506;
|
||||
}
|
||||
|
||||
#marker-md {
|
||||
color: #0090ff;
|
||||
}
|
||||
|
||||
#marker-lg {
|
||||
color: #05ff80;
|
||||
}
|
||||
|
||||
#marker-xl {
|
||||
color: #82f500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
import SeasonList from "./components/SeasonList.vue";
|
||||
import "instantsearch.css/themes/algolia-min.css";
|
||||
import Footer from "./components/Footer.vue"
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
SeasonList,
|
||||
Footer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchClient: algoliasearch(
|
||||
process.env.VUE_APP_ALGOLIA_APP_ID,
|
||||
process.env.VUE_APP_ALGOLIA_API_KEY
|
||||
),
|
||||
insightsClient: window.aa,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showBreakpointMarker() {
|
||||
return process.env.NODE_ENV === 'development';
|
||||
},
|
||||
buildMoment() {
|
||||
return moment(document.documentElement.dataset.buildTimestampUtc)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showResults() {
|
||||
if (this.$refs.searchbox.currentRefinement !== "" && this.$route.path !== "/search_results")
|
||||
this.$router.push({name: "SearchResults"});
|
||||
},
|
||||
}
|
||||
const toggleSidebar = () => {
|
||||
sidebarOpen.value = !sidebarOpen.value;
|
||||
};
|
||||
|
||||
const headings = [
|
||||
{ name: 'Home', href: '/' },
|
||||
{ name: 'Episodes', href: '/episodes' },
|
||||
{ name: 'Characters', href: '/characters' },
|
||||
{ name: 'Seasons', href: '/seasons' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="bg-white px-4 py-3 border-b border-gray-200 fixed top-0 left-0 right-0 z-40 flex items-center h-24"
|
||||
>
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Mobile menu button -->
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="lg:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Logo/Brand -->
|
||||
<div class="flex">
|
||||
<img :src="logoSrc" alt="The Office Logo" class="h-full max-w-[225px] py-4 mr-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Navigation -->
|
||||
<nav
|
||||
class="hidden text-gray-800 md:flex items-center space-x-2 font-display text-2xl tracking-widest lowercase"
|
||||
>
|
||||
<RouterLink
|
||||
v-for="heading in headings"
|
||||
:key="heading.name"
|
||||
:to="heading.href"
|
||||
class="hover:text-blue-600 px-3 py-2 transition-[color]"
|
||||
>
|
||||
{{ heading.name }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<!-- Search bar -->
|
||||
<div class="hidden md:flex items-center">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
class="w-64 pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
||||
/>
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex mt-24">
|
||||
<!-- Sidebar -->
|
||||
<div class="pl-8">
|
||||
<SeasonList />
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Overlay for mobile -->
|
||||
<div
|
||||
v-if="sidebarOpen"
|
||||
@click="toggleSidebar"
|
||||
class="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden"
|
||||
></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="col-span-8 lg:ml-0">
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl text-gray-900 mb-6">Welcome to The Office</h2>
|
||||
<p class="text-gray-600">
|
||||
This is your main content area. You can add your router-view or other components here.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.0"
|
||||
width="500"
|
||||
height="85"
|
||||
id="svg2">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<g
|
||||
transform="translate(-15.233767,-25.187374)"
|
||||
id="layer1">
|
||||
<path
|
||||
d="m 25.22234,48.597434 c 0.669198,5.9e-5 1.14158,-0.136756 1.417144,-0.410442 0.275547,-0.273569 0.413326,-0.742645 0.413335,-1.40723 l 0,-11.433741 c -9e-6,-1.055349 0.393641,-1.934866 1.180955,-2.638554 0.787291,-0.703542 1.810784,-1.055348 3.070482,-1.055424 1.25967,7.6e-5 2.312686,0.361655 3.159053,1.084739 0.846334,0.723235 1.269508,1.592979 1.269527,2.609239 l 0,11.081933 c -1.9e-5,0.977303 0.09839,1.583193 0.295238,1.817672 0.196807,0.234597 0.669189,0.351866 1.417146,0.351808 l 12.104786,0 c 1.456477,5.8e-5 2.588223,0.342093 3.395243,1.026104 0.80695,0.684127 1.210443,1.651597 1.21048,2.902411 -3.7e-5,1.407284 -0.433053,2.44316 -1.299051,3.107634 -0.866068,0.664575 -2.165117,0.996837 -3.89715,0.996787 l -11.514308,0 c -0.629862,5e-5 -1.07272,0.244361 -1.328574,0.732932 -0.255892,0.48867 -0.383829,1.358415 -0.38381,2.609239 l 0,25.212864 c -1.9e-5,6.684356 0.590458,10.945131 1.771432,12.782337 1.180932,1.837223 3.306649,2.755828 6.377154,2.755828 2.637436,0 4.684423,-0.879515 6.140964,-2.63856 1.456478,-1.759026 2.184732,-4.260765 2.184767,-7.505224 -3.5e-5,-0.664508 -0.0394,-1.68084 -0.118095,-3.048998 -0.07877,-1.368119 -0.118129,-2.38445 -0.118096,-3.048997 -3.3e-5,-1.837192 0.34441,-3.234649 1.033336,-4.192372 0.688853,-0.957671 1.702506,-1.43652 3.040958,-1.436548 1.810757,2.8e-5 3.090123,0.596147 3.838102,1.788355 0.747895,1.192261 1.121863,3.312876 1.121908,6.361851 -4.5e-5,7.075251 -1.535283,12.489171 -4.605723,16.241773 -3.070519,3.75261 -7.53846,5.62891 -13.403836,5.62893 -5.826063,-2e-5 -9.998766,-1.47565 -12.51812,-4.42692 -2.519381,-2.95127 -3.779063,-7.925427 -3.779055,-14.922497 l 0,-29.551822 c -8e-6,-1.485361 -0.127945,-2.413741 -0.38381,-2.785143 -0.255883,-0.371302 -0.69874,-0.556978 -1.328574,-0.557028 l -4.605723,0 c -1.417147,5e-5 -2.450482,-0.32244 -3.100006,-0.967471 -0.649525,-0.644928 -0.974287,-1.651487 -0.974288,-3.01968 1e-6,-1.407174 0.344446,-2.433278 1.033336,-3.078315 0.68889,-0.644922 1.781271,-0.967412 3.277149,-0.96747 l 4.605724,0 z"
|
||||
id="path2425"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 74.781443,46.076147 c -1.5e-5,-5.941569 -0.446049,-9.508502 -1.338104,-10.700809 -0.89208,-1.192164 -2.704686,-1.788281 -5.437823,-1.788354 l -3.587255,0 c -1.518416,7.3e-5 -2.600286,-0.254008 -3.245611,-0.76225 -0.645326,-0.508091 -0.96799,-1.348518 -0.96799,-2.521285 0,-1.211705 0.389095,-2.149856 1.167282,-2.814459 0.778186,-0.664445 1.907505,-0.996707 3.387962,-0.996789 0.07592,8.2e-5 0.749713,0.05872 2.021391,0.175905 1.271664,0.117348 2.533846,0.175984 3.786546,0.175902 2.733135,8.2e-5 5.466278,-0.09764 8.199441,-0.293171 0.493465,-0.03901 0.797148,-0.05855 0.911048,-0.05864 1.442474,8.1e-5 2.419953,0.303025 2.932439,0.908836 0.512442,0.605968 0.768674,2.06206 0.768697,4.368275 l 0,25.212864 c 2.505359,-3.283483 5.285953,-5.687497 8.341792,-7.212052 3.055775,-1.52444 6.576595,-2.286689 10.562472,-2.286748 7.89571,5.9e-5 13.59924,2.003406 17.11063,6.010043 3.51128,4.006745 5.26694,10.485859 5.267,19.43736 l 0,18.997601 c -6e-5,3.674444 0.35108,5.970963 1.05341,6.889561 0.7022,0.918616 2.19214,1.377923 4.46983,1.377913 0.45545,1e-5 1.01538,-0.0586 1.67975,-0.1759 0.66422,-0.117265 1.11026,-0.1759 1.3381,-0.175907 1.59425,7e-6 2.80899,0.342047 3.64419,1.026107 0.83506,0.68407 1.25263,1.71018 1.2527,3.07831 -7e-5,1.28996 -0.35121,2.20858 -1.05341,2.75583 -0.70234,0.54725 -1.86961,0.82089 -3.50184,0.82089 -0.41764,0 -2.01196,-0.15636 -4.78301,-0.46908 -2.77117,-0.31272 -5.33349,-0.46908 -7.68697,-0.46908 -2.80913,0 -5.73207,0.15636 -8.76885,0.46908 -3.03687,0.31272 -4.66916,0.46908 -4.89689,0.46908 -1.32865,0 -2.35358,-0.3225 -3.07478,-0.96748 -0.72128,-0.64497 -1.08192,-1.55381 -1.08187,-2.72651 -5e-5,-1.36813 0.37006,-2.37469 1.11034,-3.01968 0.74018,-0.64497 1.8695,-0.967459 3.38796,-0.967466 0.68325,7e-6 1.4804,0.05864 2.39151,0.175906 0.911,0.11727 1.48039,0.17591 1.70822,0.1759 1.59428,1e-5 2.65717,-0.449524 3.18866,-1.348594 0.53139,-0.899054 0.79711,-3.205345 0.79717,-6.918879 l 0,-17.06266 c -6e-5,-7.231558 -1.15785,-12.381622 -3.47337,-15.45021 -2.31564,-3.068491 -6.13065,-4.602761 -11.44505,-4.602813 -6.073697,5.2e-5 -10.515055,2.149984 -13.324092,6.449803 -2.809093,4.299909 -4.213625,11.121058 -4.213602,20.463465 l 0,10.202415 c -2.3e-5,3.713534 0.265699,6.019825 0.797168,6.918879 0.531421,0.89907 1.59431,1.348604 3.188671,1.348594 0.227735,1e-5 0.80663,-0.0586 1.736687,-0.1759 0.929999,-0.117265 1.717676,-0.175899 2.363033,-0.175906 1.518386,7e-6 2.638216,0.322496 3.359488,0.967466 0.72122,0.64499 1.08184,1.65155 1.08187,3.01968 -3e-5,1.21179 -0.35117,2.1304 -1.0534,2.75583 -0.702295,0.62544 -1.71773,0.93816 -3.046315,0.93816 -0.227792,0 -1.860087,-0.15636 -4.896887,-0.46908 -3.036853,-0.31272 -5.959799,-0.46908 -8.768847,-0.46908 -2.315596,0 -4.868429,0.15636 -7.658504,0.46908 -2.790093,0.31272 -4.393918,0.46908 -4.811477,0.46908 -1.632299,0 -2.799579,-0.27363 -3.501844,-0.82089 -0.702268,-0.54725 -1.0534,-1.46586 -1.0534,-2.75583 0,-1.36813 0.398584,-2.39424 1.195752,-3.07831 0.797164,-0.68406 1.992916,-1.0261 3.587254,-1.026107 0.417559,7e-6 0.996454,0.05864 1.736689,0.175907 0.740218,0.11727 1.262172,0.17591 1.565864,0.1759 2.239651,1e-5 3.729593,-0.410433 4.469834,-1.231325 0.740213,-0.820875 1.110326,-3.166256 1.110341,-7.036148 l 0,-45.852234 z"
|
||||
id="path2423"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 163.72346,46.663801 c -8.06744,6e-5 -14.46922,2.796936 -19.20689,8.40625 -4.73772,5.60942 -7.0918,13.15749 -7.09177,22.656251 -1e-5,9.537909 2.35404,17.109383 7.09177,22.718748 4.73768,5.60937 11.13945,8.43748 19.20689,8.4375 4.52837,-2e-5 8.43464,-0.7646 11.68829,-2.25 3.25354,-1.48541 6.04568,-3.77907 8.40506,-6.906248 1.52209,-2.071747 2.73104,-4.158692 3.64438,-6.25 0.91324,-2.091284 1.37892,-3.842754 1.37896,-5.25 -5e-5,-1.211761 -0.38813,-2.198638 -1.14914,-3 -0.76114,-0.801318 -1.71484,-1.21873 -2.85641,-1.21875 -1.78857,2.2e-5 -3.25274,1.607169 -4.43235,4.8125 -0.76112,2.14995 -1.48201,3.757093 -2.16694,4.8125 -1.78857,2.619021 -3.78741,4.514819 -5.97546,5.6875 -2.18813,1.172698 -4.88026,1.750008 -8.07675,1.749998 -5.25145,1e-5 -9.46295,-1.981591 -12.64043,-5.968748 -3.1775,-3.987134 -4.76069,-9.300067 -4.76068,-15.90625 l 0,-0.34375 34.93355,0 2.00277,0 c 2.20706,2.8e-5 3.67121,-0.374428 4.43235,-1.15625 0.76102,-0.781766 1.14907,-2.349924 1.14914,-4.65625 -7e-5,-8.01335 -2.31793,-14.398963 -6.96045,-19.187501 -4.64261,-4.788432 -10.85295,-7.18744 -18.61589,-7.1875 z m 0.13132,7.6875 c 4.68058,5.3e-5 8.4488,1.466734 11.35996,4.4375 2.91108,2.970863 4.36665,6.856101 4.3667,11.625001 l 0,0.75 -32.24129,0 c 0.49468,-5.198891 2.23831,-9.302549 5.1875,-12.312501 2.94915,-3.009856 6.72259,-4.499948 11.32713,-4.5 z"
|
||||
id="path2421"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 254.96172,46.788801 c -8.31131,6.1e-5 -14.89983,2.773524 -19.76582,8.34375 -4.86601,5.57033 -7.29342,13.09499 -7.29343,22.593751 0,9.498819 2.42742,17.089842 7.29343,22.718748 4.86599,5.62892 11.43362,8.43748 19.67388,8.4375 8.48884,-2e-5 15.16266,-2.79301 20.01097,-8.34375 4.8482,-5.550725 7.26274,-13.157318 7.26279,-22.812498 -5e-5,-9.576942 -2.4146,-17.125007 -7.26279,-22.656251 -4.8483,-5.531136 -11.50124,-8.281187 -19.91903,-8.28125 z m 0.12258,7.6875 c 5.75393,5.2e-5 10.27721,2.05188 13.54494,6.15625 3.26764,4.104463 4.90309,9.784013 4.90315,17.093751 -5e-5,7.231618 -1.64841,12.907304 -4.93379,17.03125 -3.28549,4.123973 -7.79587,6.187508 -13.5143,6.187498 -5.93159,1e-5 -10.51443,-2.005001 -13.72882,-6.031248 -3.21442,-4.026224 -4.81122,-9.760431 -4.81121,-17.1875 -1e-5,-7.27065 1.64834,-12.969742 4.93379,-17.093751 3.28542,-4.123913 7.81675,-6.156197 13.60624,-6.15625 z"
|
||||
id="path2417"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 299.37594,56.04936 c -2.06407,0.03914 -4.21411,0.107547 -6.45015,0.205221 -2.23607,0.09777 -3.44965,0.146637 -3.64075,0.146586 -1.29959,5.1e-5 -2.25516,-0.273577 -2.86673,-0.820884 -0.61157,-0.547204 -0.91736,-1.407176 -0.91736,-2.57992 0,-1.172636 0.4109,-2.101017 1.23269,-2.785143 0.82179,-0.684012 1.93983,-1.026046 3.35408,-1.026105 0.15289,5.9e-5 1.08936,0.05869 2.8094,0.175905 1.72003,0.117327 3.36362,0.175961 4.93078,0.175902 l 1.54804,0 c -1e-5,-0.27357 -0.0191,-0.674239 -0.0573,-1.202007 -0.0382,-0.527651 -0.0574,-0.928323 -0.0573,-1.202009 -2e-5,-6.606095 1.64357,-11.619345 4.93078,-15.039767 3.28717,-3.420271 8.10326,-5.130445 14.44834,-5.130525 4.39562,8e-5 7.9217,0.987094 10.57825,2.961046 2.65646,1.974106 3.98471,4.583342 3.98475,7.827715 -4e-5,1.759105 -0.47783,3.166333 -1.43337,4.221689 -0.95561,1.055486 -2.23609,1.583197 -3.84142,1.583133 -1.68186,6.4e-5 -3.01011,-0.390833 -3.98476,-1.172691 -0.97472,-0.781729 -1.46207,-1.83715 -1.46203,-3.166267 -4e-5,-0.469008 0.086,-1.12376 0.25801,-1.964258 0.17197,-0.840357 0.25796,-1.436476 0.258,-1.788355 -4e-5,-0.547183 -0.36316,-0.957624 -1.08936,-1.231326 -0.72627,-0.273554 -1.85385,-0.410368 -3.38274,-0.410441 -3.47833,7.3e-5 -6.11572,1.075039 -7.91218,3.224901 -1.79652,2.150003 -2.69476,5.316266 -2.69473,9.498801 -3e-5,0.781854 0.01,1.377971 0.0287,1.788354 0.0191,0.410501 0.0669,0.81117 0.14333,1.202008 2.56094,5.8e-5 5.23654,-0.09766 8.02687,-0.293172 0.64975,-0.03903 1.05109,-0.05858 1.20402,-0.05863 1.56712,5.8e-5 2.7807,0.322547 3.64075,0.96747 0.85998,0.645037 1.29,1.553872 1.29004,2.726507 -4e-5,1.172745 -0.3345,2.052263 -1.00337,2.638557 -0.66893,0.586396 -1.69141,0.879569 -3.0674,0.879518 -0.15292,5e-5 -0.55427,-0.01949 -1.20402,-0.05863 -2.98144,-0.195397 -5.86728,-0.293121 -8.65754,-0.293173 l 0,36.236165 c -3e-5,3.674444 0.28665,5.970963 0.86002,6.889561 0.57332,0.918614 1.75823,1.377914 3.55475,1.377914 0.15287,0 0.74531,-0.0586 1.77737,-0.17591 1.032,-0.11726 1.96847,-0.17589 2.8094,-0.1759 1.72,1e-5 2.97181,0.3225 3.75542,0.96747 0.78354,0.64499 1.17533,1.65155 1.17537,3.01968 -4e-5,1.21178 -0.38228,2.13039 -1.1467,2.75582 -0.7645,0.62544 -1.87297,0.93816 -3.32541,0.93816 -0.2676,0 -2.04498,-0.15636 -5.33213,-0.46908 -3.28721,-0.31272 -6.44061,-0.46908 -9.46022,-0.46908 -2.56096,0 -5.25568,0.15636 -8.08418,0.46908 -2.82852,0.31272 -4.39567,0.46908 -4.70145,0.46908 -1.6436,0 -2.83806,-0.28341 -3.58342,-0.85021 -0.74535,-0.5668 -1.11802,-1.47563 -1.11802,-2.7265 0,-1.36814 0.42044,-2.39424 1.26137,-3.07831 0.8409,-0.68407 2.06404,-1.0261 3.6694,-1.02611 0.42046,1e-5 1.00337,0.0586 1.74872,0.1759 0.74534,0.11728 1.27091,0.17591 1.5767,0.17591 2.25515,0 3.75541,-0.41044 4.50078,-1.231327 0.74533,-0.820875 1.11801,-3.166256 1.11802,-7.036148 l 0,-36.236164 z"
|
||||
id="path2413"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 341.651,56.04936 c -2.0486,0.03914 -4.18256,0.107547 -6.40184,0.205221 -2.21932,0.09777 -3.42382,0.146637 -3.61351,0.146586 -1.28986,5.1e-5 -2.23828,-0.273577 -2.84527,-0.820884 -0.60699,-0.547204 -0.91049,-1.407176 -0.91049,-2.57992 0,-1.172636 0.40783,-2.101017 1.22348,-2.785143 0.81564,-0.684012 1.92529,-1.026046 3.32896,-1.026105 0.15174,5.9e-5 1.0812,0.05869 2.78837,0.175905 1.70714,0.117327 3.33843,0.175961 4.89386,0.175902 l 1.53644,0 c -1e-5,-0.27357 -0.019,-0.674239 -0.0569,-1.202007 -0.0379,-0.527651 -0.0569,-0.928323 -0.0569,-1.202009 -10e-6,-6.606095 1.63128,-11.619345 4.89387,-15.039767 3.26256,-3.420271 8.04261,-5.130445 14.34015,-5.130525 4.36272,8e-5 7.8624,0.987094 10.49906,2.961046 2.63658,1.974106 3.95488,4.583342 3.95494,7.827715 -6e-5,1.759105 -0.47427,3.166333 -1.42264,4.221689 -0.94848,1.055486 -2.21936,1.583197 -3.81267,1.583133 -1.66926,6.4e-5 -2.98757,-0.390833 -3.95492,-1.172691 -0.96743,-0.781729 -1.45112,-1.83715 -1.45109,-3.166267 -3e-5,-0.469008 0.0853,-1.12376 0.25607,-1.964258 0.17067,-0.840357 0.25604,-1.436476 0.25607,-1.788355 -3e-5,-0.547183 -0.36043,-0.957624 -1.08119,-1.231326 -0.72085,-0.273554 -1.83998,-0.410368 -3.35742,-0.410441 -3.4523,7.3e-5 -6.06994,1.075039 -7.85295,3.224901 -1.78306,2.150003 -2.67458,5.316266 -2.67456,9.498801 -2e-5,0.781854 0.009,1.377971 0.0285,1.788354 0.0189,0.410501 0.0664,0.81117 0.14226,1.202008 2.54174,5.8e-5 5.19734,-0.09766 7.96676,-0.293172 0.6449,-0.03903 1.04323,-0.05858 1.19501,-0.05863 1.55538,5.8e-5 2.75988,0.322547 3.61349,0.96747 0.85355,0.645037 1.28034,1.553872 1.28038,2.726507 -4e-5,1.172745 -0.33199,2.052263 -0.99584,2.638557 -0.66394,0.586396 -1.67875,0.879569 -3.04445,0.879518 -0.15178,5e-5 -0.55012,-0.01949 -1.19501,-0.05863 -2.95912,-0.195397 -5.82335,-0.293121 -8.59272,-0.293173 l 0,36.236165 c -2e-5,3.674444 0.2845,5.970963 0.85358,6.889561 0.56902,0.918614 1.74507,1.377914 3.52814,1.377914 0.15171,0 0.73975,-0.0586 1.76407,-0.17591 1.02426,-0.11726 1.95371,-0.17589 2.78836,-0.1759 1.70713,1e-5 2.94957,0.3225 3.7273,0.96747 0.77767,0.64499 1.16653,1.65154 1.16657,3.01968 -4e-5,1.21178 -0.3794,2.13039 -1.13811,2.75582 -0.75878,0.62544 -1.85895,0.93816 -3.30052,0.93816 -0.26558,0 -2.02965,-0.15636 -5.2922,-0.46908 -3.2626,-0.31272 -6.3924,-0.46908 -9.38938,-0.46908 -2.54181,0 -5.21635,0.15636 -8.02368,0.46908 -2.80734,0.31272 -4.36275,0.46908 -4.66624,0.46908 -1.63128,0 -2.81683,-0.28341 -3.55659,-0.85021 -0.73977,-0.5668 -1.10966,-1.47563 -1.10966,-2.7265 0,-1.36814 0.41732,-2.39424 1.25192,-3.07831 0.83462,-0.68407 2.0486,-1.0261 3.64196,-1.02611 0.4173,1e-5 0.99583,0.0586 1.7356,0.1759 0.73978,0.11728 1.26141,0.17591 1.56491,0.17591 2.23827,0 3.72729,-0.41044 4.46707,-1.231327 0.73976,-0.820875 1.10964,-3.166256 1.10965,-7.036148 l 0,-36.236164 z"
|
||||
id="path2411"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 384.71058,92.285525 0,-23.922905 c -10e-6,-5.511606 -0.36221,-8.941725 -1.08659,-10.290368 -0.72441,-1.348544 -2.20943,-2.022841 -4.45505,-2.022892 l -4.02041,0 c -1.34014,5.1e-5 -2.26375,-0.254032 -2.77082,-0.76225 -0.50708,-0.508113 -0.76062,-1.387631 -0.76062,-2.638555 0,-1.055367 0.37126,-1.954429 1.11376,-2.69719 0.74251,-0.742647 1.65706,-1.113999 2.74366,-1.114058 0.21732,5.9e-5 1.04131,0.05869 2.47201,0.175905 1.43068,0.117326 2.76176,0.175961 3.99324,0.175903 2.06452,5.8e-5 4.21961,-0.09767 6.46526,-0.293173 0.54328,-0.03903 0.86925,-0.05858 0.97794,-0.05863 1.52121,5.9e-5 2.53537,0.293231 3.04247,0.879519 0.50705,0.586403 0.7606,1.934997 0.76062,4.045785 l 0,38.522913 c -2e-5,3.596265 0.33501,5.892783 1.0051,6.889561 0.67004,0.9968 2.07357,1.49519 4.21056,1.49518 0.47083,1e-5 1.01413,-0.0586 1.6299,-0.1759 0.6157,-0.11726 1.05034,-0.1759 1.30392,-0.17591 1.55742,1e-5 2.72551,0.33227 3.50428,0.99679 0.77869,0.66453 1.16804,1.66132 1.16808,2.99036 -4e-5,1.28996 -0.33507,2.20857 -1.0051,2.75582 -0.6701,0.54726 -1.78386,0.8209 -3.34128,0.8209 -0.43468,0 -1.95591,-0.15637 -4.56371,-0.46909 -2.60786,-0.31272 -4.98026,-0.46908 -7.11721,-0.46908 -4.02044,0 -8.09517,0.25409 -12.22423,0.76225 -0.9055,0.11727 -1.46691,0.17591 -1.68422,0.17591 -1.59368,0 -2.7346,-0.28341 -3.42278,-0.85021 -0.68818,-0.5668 -1.03227,-1.47563 -1.03227,-2.7265 0,-1.32904 0.38936,-2.32583 1.16809,-2.99036 0.77873,-0.66452 1.94682,-0.99678 3.50428,-0.99679 0.3622,1e-5 0.88738,0.0586 1.57556,0.17591 0.68818,0.11728 1.19525,0.17591 1.52124,0.1759 2.2094,1e-5 3.64914,-0.47884 4.31922,-1.436545 0.67005,-0.957689 1.00509,-3.273752 1.0051,-6.948196 l 0,0 z"
|
||||
id="path2409"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 380.68219,36.572011 c -10e-6,-1.837144 0.61462,-3.381186 1.84388,-4.632131 1.22924,-1.250794 2.74661,-1.876229 4.5521,-1.876306 1.72863,7.7e-5 3.17876,0.625512 4.35042,1.876306 1.17162,1.250945 1.75744,2.794987 1.75747,4.632131 -3e-5,1.759106 -0.59546,3.264058 -1.78628,4.514862 -1.19086,1.250936 -2.63139,1.876371 -4.32161,1.876306 -1.80549,6.5e-5 -3.32286,-0.615598 -4.5521,-1.846988 -1.22926,-1.23126 -1.84389,-2.745985 -1.84388,-4.54418 l 0,0 z"
|
||||
id="path2407"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 404.7175,77.738815 c 0,-9.694209 2.40243,-17.316697 7.20734,-22.867482 4.80488,-5.550682 11.39283,-8.326049 19.76389,-8.326109 6.75686,6e-5 12.20928,1.632054 16.35731,4.895986 4.14793,3.264044 6.22191,7.515046 6.22197,12.753021 -6e-5,3.049037 -0.83528,5.472595 -2.50568,7.270685 -1.67051,1.79816 -3.93219,2.697223 -6.78505,2.697191 -1.95203,3.3e-5 -3.46295,-0.459271 -4.53274,-1.377912 -1.06988,-0.918573 -1.60481,-2.218305 -1.60477,-3.899199 -4e-5,-1.133563 0.28151,-2.16944 0.84461,-3.107632 0.56304,-0.938112 1.35134,-1.70036 2.36491,-2.286749 0.18766,-0.117225 0.46919,-0.234495 0.84461,-0.351807 0.82581,-0.351764 1.23873,-0.879474 1.23877,-1.583133 -4e-5,-1.87626 -1.14496,-3.566888 -3.43475,-5.07189 -2.28987,-1.504902 -5.03016,-2.257379 -8.22088,-2.257432 -5.93107,5.3e-5 -10.53889,2.140213 -13.82346,6.420485 -3.28462,4.280367 -4.92692,10.309949 -4.92691,18.088766 -1e-5,6.801631 1.58598,12.264413 4.75798,16.388362 3.17196,4.123973 7.34809,6.185954 12.5284,6.185944 3.49102,1e-5 6.38146,-0.75247 8.67133,-2.257428 2.28979,-1.504944 4.16671,-3.899187 5.63075,-7.182735 0.33779,-0.742689 0.76948,-1.915379 1.29507,-3.518074 1.27624,-3.713501 2.87162,-5.570261 4.78612,-5.570284 1.20118,2.3e-5 2.19593,0.400694 2.98429,1.202009 0.78826,0.801361 1.18241,1.80792 1.18247,3.01968 -6e-5,1.798145 -0.4599,3.918759 -1.37954,6.36185 -0.91974,2.443118 -2.11158,4.641913 -3.57552,6.596389 -2.25234,2.931733 -5.04894,5.150073 -8.38981,6.655023 -3.34094,1.50496 -7.1886,2.25742 -11.54301,2.25744 -7.99567,-2e-5 -14.32085,-2.79492 -18.97559,-8.38475 -4.65475,-5.589818 -6.98211,-13.173214 -6.98211,-22.750214 l 0,0 z"
|
||||
id="path2405"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
<path
|
||||
d="m 488.0568,47.020944 c -8.23406,6e-5 -14.76803,2.780881 -19.60358,8.357997 -4.83556,5.577221 -7.23825,13.081964 -7.23825,22.5262 -10e-6,9.483161 2.4027,17.011174 7.23825,22.588339 4.83554,5.57717 11.36952,8.38905 19.60358,8.38907 4.62189,-2e-5 8.60887,-0.76021 11.9297,-2.23708 3.32076,-1.47689 6.17055,-3.75738 8.57866,-6.866611 1.55354,-2.059854 2.78744,-4.13482 3.71966,-6.214124 0.93212,-2.07928 1.40739,-3.820696 1.40744,-5.219865 -6e-5,-1.204805 -0.3961,-2.186016 -1.17286,-2.982779 -0.77686,-0.796719 -1.75031,-1.211735 -2.91541,-1.211755 -1.82553,2.2e-5 -3.31992,1.597944 -4.5239,4.784876 -0.77685,2.137609 -1.51261,3.735527 -2.21169,4.784876 -1.82551,2.603987 -3.86565,4.488903 -6.09889,5.654853 -2.23333,1.165969 -4.98106,1.739959 -8.24356,1.739959 -5.35993,0 -9.65841,-1.970222 -12.9015,-5.934493 -3.24314,-3.964247 -4.85901,-9.246684 -4.85901,-15.814946 l 0,-0.341777 35.65506,0 2.04414,0 c 2.25266,2.8e-5 3.74702,-0.372279 4.5239,-1.149613 0.77674,-0.777278 1.17283,-2.336435 1.17286,-4.629523 -5e-5,-7.967351 -2.3658,-14.31631 -7.1042,-19.077361 -4.7385,-4.760946 -11.07712,-7.146183 -19.0004,-7.146243 z m 0.13405,7.643374 c 4.77725,5.1e-5 8.62332,1.458314 11.59459,4.412028 2.9712,2.953809 4.45683,6.816744 4.45688,11.558271 l 0,0.745695 -32.90721,0 c 0.5049,-5.169049 2.28456,-9.249151 5.29465,-12.241826 3.01007,-2.992579 6.86143,-4.474117 11.56109,-4.474168 z"
|
||||
id="path2403"
|
||||
style="font-size:115.59156036px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:AmerType Md BT;-inkscape-font-specification:AmerType Md BT" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
+50
-50
@@ -1,55 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb :items="breadcrumbs" />
|
||||
<b-card>
|
||||
<h3>What is this?</h3>
|
||||
<p>
|
||||
This app is a quote viewing and searching app centered around the hit workplace comedy
|
||||
and sitcom 'The Office'. I made it in order to learn Vue, API creation and a variety of
|
||||
techniques and frameworks. Since inception, it's evolved and improved from something simple to
|
||||
a passion project where I have poured a great deal of work into.
|
||||
</p>
|
||||
<h3>How does it work?</h3>
|
||||
<p>
|
||||
Most of the UI is self-explanatory and designed to be easy to use.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
You can select episodes with the drop-down based selector on the left.
|
||||
</li>
|
||||
<li>
|
||||
You can search for quotes with the search bar at the top left.
|
||||
</li>
|
||||
<li>
|
||||
You can browse characters by clicking the 'Characters' link at the top or by finding
|
||||
and clicking on individual characters inside the episodes themselves.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>I found an error in the quotes, how can I help fix it?</h3>
|
||||
<p>
|
||||
Currently, this part of the application is rather difficult to explain the workings of.
|
||||
There will be a system in place to explain how errors can be amended and taken care of,
|
||||
but that will be explained and added later once I have time.
|
||||
For now, please open a new issue on <a href="https://github.com/Xevion/the-office/issues/new">the github</a>.
|
||||
</p>
|
||||
</b-card>
|
||||
</div>
|
||||
<div>
|
||||
<BBreadcrumb :items="breadcrumbs" />
|
||||
<BCard>
|
||||
<h3>What is this?</h3>
|
||||
<p>
|
||||
This app is a quote viewing and searching app centered around the hit workplace comedy and
|
||||
sitcom 'The Office'. I made it in order to learn Vue, API creation and a variety of
|
||||
techniques and frameworks. Since inception, it's evolved and improved from something simple
|
||||
to a passion project where I have poured a great deal of work into.
|
||||
</p>
|
||||
<h3>How does it work?</h3>
|
||||
<p>Most of the UI is self-explanatory and designed to be easy to use.</p>
|
||||
<ul>
|
||||
<li>You can select episodes with the drop-down based selector on the left.</li>
|
||||
<li>You can search for quotes with the search bar at the top left.</li>
|
||||
<li>
|
||||
You can browse characters by clicking the 'Characters' link at the top or by finding and
|
||||
clicking on individual characters inside the episodes themselves.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>I found an error in the quotes, how can I help fix it?</h3>
|
||||
<p>
|
||||
Currently, this part of the application is rather difficult to explain the workings of.
|
||||
There will be a system in place to explain how errors can be amended and taken care of, but
|
||||
that will be explained and added later once I have time. For now, please open a new issue on
|
||||
<a href="https://github.com/Xevion/the-office/issues/new">the github</a>.
|
||||
</p>
|
||||
</BCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "About",
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{text: 'Home', to: {name: 'Home'}},
|
||||
{text: 'About', active: true}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { BBreadcrumb } from 'bootstrap-vue-next'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AboutComponent',
|
||||
|
||||
components: {
|
||||
BBreadcrumb,
|
||||
},
|
||||
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{ text: 'Home', to: { name: 'Home' } },
|
||||
{ text: 'About', active: true },
|
||||
]
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,87 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs"></b-breadcrumb>
|
||||
<b-card v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%;"></Skeleton>
|
||||
</b-card>
|
||||
<b-card>
|
||||
<h4 v-if="ready">{{ character.name }}</h4>
|
||||
<Skeleton v-else style="max-width: 30%"></Skeleton>
|
||||
<b-card-body v-if="ready">
|
||||
{{ character.summary }}
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
<div>
|
||||
<BBreadcrumb v-if="ready" :items="breadcrumbs" />
|
||||
<BCard v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%"></Skeleton>
|
||||
</BCard>
|
||||
<BCard>
|
||||
<h4 v-if="ready">{{ character?.name }}</h4>
|
||||
<Skeleton v-else style="max-width: 30%"></Skeleton>
|
||||
<BCard-body v-if="ready">
|
||||
{{ character?.summary }}
|
||||
</BCard-body>
|
||||
</BCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb-skeleton {
|
||||
background-color: $grey-3;
|
||||
height: 48px;
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.breadcrumb-skeleton {
|
||||
background-color: $gray-100;
|
||||
height: 48px;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import {types} from "@/mutation_types";
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
import Skeleton from './Skeleton.vue'
|
||||
import { BBreadcrumb } from 'bootstrap-vue-next'
|
||||
import useStore from '@/store'
|
||||
|
||||
export default {
|
||||
name: 'Character',
|
||||
components: {
|
||||
Skeleton,
|
||||
interface BreadcrumbItem {
|
||||
text: string
|
||||
to?: { name: string }
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CharacterPage',
|
||||
|
||||
components: {
|
||||
Skeleton,
|
||||
BBreadcrumb,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const store = useStore()
|
||||
return {
|
||||
store,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
character() {
|
||||
return this.store.characters[this.$route.params.character as string]
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
character: null
|
||||
}
|
||||
ready(): boolean {
|
||||
return this.character !== undefined
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.character !== undefined && this.character !== null;
|
||||
|
||||
breadcrumbs(): BreadcrumbItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: { name: 'Home' },
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {name: 'Home'},
|
||||
},
|
||||
{
|
||||
text: 'Characters',
|
||||
to: {name: 'Characters'},
|
||||
},
|
||||
{
|
||||
text:
|
||||
this.character !== null && this.character !== undefined
|
||||
? this.character.name || this.$route.params.character
|
||||
: this.$route.params.character,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
{
|
||||
text: 'Characters',
|
||||
to: { name: 'Characters' },
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.$nextTick(() => {
|
||||
this.fetchCharacter();
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchCharacter();
|
||||
},
|
||||
methods: {
|
||||
fetchCharacter() {
|
||||
this.$store.dispatch(types.PRELOAD_CHARACTERS)
|
||||
.then(() => {
|
||||
this.character = this.$store.getters.getCharacter(this.$route.params.character);
|
||||
})
|
||||
{
|
||||
text: this.character?.name || (this.$route.params.character as string),
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.params.character'() {
|
||||
nextTick(() => {
|
||||
this.fetchCharacter()
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchCharacter()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchCharacter(): Promise<void> {
|
||||
try {
|
||||
await this.store.preloadCharacters()
|
||||
this.character = this.store.characters[this.$route.params.character as string]
|
||||
} catch (error) {
|
||||
console.error('Error fetching character:', error)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
<template>
|
||||
<div v-if="characters" class="pt-2" :fluid="true">
|
||||
<b-button
|
||||
v-for="(character, character_id) in characters" :id="`character-${character_id}`"
|
||||
:key="character.name" squared
|
||||
class="mx-2 my-1 character-button" size="sm"
|
||||
:title="`${character.appearances} Quote${character.appearances > 1 ? 's' : ''}`"
|
||||
:to="{ name: 'Character', params: { character: character_id } }"
|
||||
>
|
||||
{{ character.name }}
|
||||
<b-badge class="ml-1">
|
||||
{{ character.appearances }}
|
||||
</b-badge>
|
||||
</b-button>
|
||||
</div>
|
||||
<div v-if="characters" class="pt-2" :fluid="true">
|
||||
<BButton
|
||||
v-for="(character, character_id) in characters"
|
||||
:id="`character-${character_id}`"
|
||||
:key="character.name"
|
||||
squared
|
||||
class="mx-2 my-1 character-button"
|
||||
size="sm"
|
||||
:title="`${character.appearances} Quote${character.appearances > 1 ? 's' : ''}`"
|
||||
:to="{ name: 'Character', params: { character: character_id } }"
|
||||
>
|
||||
{{ character.name }}
|
||||
<BBadge class="ml-1">
|
||||
{{ character.appearances }}
|
||||
</BBadge>
|
||||
</BButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
characters: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { BButton, BBadge } from 'bootstrap-vue-next'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BButton,
|
||||
BBadge,
|
||||
},
|
||||
|
||||
props: {
|
||||
characters: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
+124
-110
@@ -1,123 +1,137 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="ready">
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs" />
|
||||
<b-card>
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="id in sorted_character_ids" :key="id">
|
||||
<b-row align-v="start" align-content="start">
|
||||
<b-col cols="5" md="4" lg="4" xl="3">
|
||||
<b-img-lazy
|
||||
fluid-grow class="rounded-sm"
|
||||
:src="faceURL(id)"
|
||||
:blank-src="faceURL(id, true)"
|
||||
width="200" height="200"
|
||||
blank-width="200" blank-height="200"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<h4>
|
||||
{{ characters[id].name || id }}
|
||||
<router-link
|
||||
class="no-link"
|
||||
:to="{ name: 'Character', params: {character: id} }"
|
||||
>
|
||||
<b-icon class="h6" icon="caret-right-fill" />
|
||||
</router-link>
|
||||
<span class="h6 font-italic" style="opacity: 50%;">
|
||||
{{ characters[id].actor }}
|
||||
</span>
|
||||
</h4>
|
||||
<p class="pl-3">
|
||||
{{ characters[id].summary }}
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</template>
|
||||
<template v-else>
|
||||
<b-card class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton class="inlined" style="width: 10%;" />
|
||||
<Skeleton class="inlined" style="width: 30%;" />
|
||||
</b-card>
|
||||
<b-card>
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="i in 6" :key="i">
|
||||
<b-row align-v="start" align-content="start">
|
||||
<b-col cols="5" lg="4" xl="3">
|
||||
<ImageSkeleton style="width: 200px; height: 200px" />
|
||||
</b-col>
|
||||
<b-col>
|
||||
<Skeleton style="width: 40%; height: 2.7em;" />
|
||||
<Skeleton style="width: 60%;" />
|
||||
<Skeleton style="width: 25%;" />
|
||||
<Skeleton style="width: 35%;" />
|
||||
<Skeleton style="width: 60%;" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="ready">
|
||||
<BBreadcrumb v-if="ready" :items="breadcrumbs" />
|
||||
<BCard>
|
||||
<BListGroup>
|
||||
<BListGroupItem v-for="id in sorted_character_ids" :key="id">
|
||||
<BRow align-v="start" align-content="start">
|
||||
<BCol cols="5" md="4" lg="4" xl="3">
|
||||
<BImg
|
||||
fluid-grow
|
||||
class="rounded-sm"
|
||||
:src="faceURL(id)"
|
||||
:blank-src="faceURL(id, true)"
|
||||
width="200"
|
||||
height="200"
|
||||
blank-width="200"
|
||||
blank-height="200"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<h4>
|
||||
{{ characters[id].name || id }}
|
||||
<RouterLink
|
||||
class="no-link"
|
||||
:to="{ name: 'Character', params: { character: id } }"
|
||||
>
|
||||
<!-- <b-icon class="h6" icon="caret-right-fill" /> -->
|
||||
</RouterLink>
|
||||
<span class="h6 font-italic" style="opacity: 50%">
|
||||
{{ characters[id].actor }}
|
||||
</span>
|
||||
</h4>
|
||||
<p class="pl-3">
|
||||
{{ characters[id].summary }}
|
||||
</p>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BListGroupItem>
|
||||
</BListGroup>
|
||||
</BCard>
|
||||
</template>
|
||||
<template v-else>
|
||||
<BCard class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton class="inlined" style="width: 10%" />
|
||||
<Skeleton class="inlined" style="width: 30%" />
|
||||
</BCard>
|
||||
<BCard>
|
||||
<BListGroup>
|
||||
<BListGroupItem v-for="i in 6" :key="i">
|
||||
<BRow align-v="start" align-content="start">
|
||||
<BCol cols="5" lg="4" xl="3">
|
||||
<ImageSkeleton style="width: 200px; height: 200px" />
|
||||
</BCol>
|
||||
<BCol>
|
||||
<Skeleton style="width: 40%; height: 2.7em" />
|
||||
<Skeleton style="width: 60%" />
|
||||
<Skeleton style="width: 25%" />
|
||||
<Skeleton style="width: 35%" />
|
||||
<Skeleton style="width: 60%" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BListGroupItem>
|
||||
</BListGroup>
|
||||
</BCard>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
@use 'sass:color';
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
&:hover {
|
||||
color: darken(#007fe0, 10%);
|
||||
}
|
||||
h4 {
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
|
||||
&:hover {
|
||||
color: color.adjust(#007fe0, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {types} from "@/mutation_types";
|
||||
import Skeleton from "@/components/Skeleton.vue";
|
||||
import ImageSkeleton from "@/components/ImageSkeleton";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ImageSkeleton,
|
||||
Skeleton
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.getters.checkPreloaded('characters');
|
||||
},
|
||||
sorted_character_ids() {
|
||||
return this.$store.getters.getSortedCharacters();
|
||||
},
|
||||
characters() {
|
||||
return this.$store.state.characters;
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{text: 'Home', to: {name: 'Home'}},
|
||||
{text: 'Characters', active: true}
|
||||
]
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.$store.dispatch(types.PRELOAD_CHARACTERS)
|
||||
import Skeleton from '@/components/Skeleton.vue'
|
||||
import ImageSkeleton from '@/components/ImageSkeleton.vue'
|
||||
import { BBreadcrumb, BImg } from 'bootstrap-vue-next'
|
||||
|
||||
// Re-compute computed properties since Vuex won't do it
|
||||
// this.$forceUpdate();
|
||||
export default defineComponent({
|
||||
name: 'CharactersComponent',
|
||||
|
||||
components: {
|
||||
ImageSkeleton,
|
||||
Skeleton,
|
||||
BBreadcrumb,
|
||||
BImg,
|
||||
},
|
||||
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.getters.checkPreloaded('characters')
|
||||
},
|
||||
methods: {
|
||||
faceURL(character, thumbnail = false) {
|
||||
return `/img/${character}/` + (thumbnail ? "face_thumb" : "face") + ".jpeg";
|
||||
}
|
||||
}
|
||||
}
|
||||
sorted_character_ids() {
|
||||
return this.$store.getters.getSortedCharacters()
|
||||
},
|
||||
characters() {
|
||||
return this.$store.state.characters
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{ text: 'Home', to: { name: 'Home' } },
|
||||
{ text: 'Characters', active: true },
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.$store.dispatch(types.PRELOAD_CHARACTERS)
|
||||
|
||||
// Re-compute computed properties since Vuex won't do it
|
||||
// this.$forceUpdate();
|
||||
},
|
||||
|
||||
methods: {
|
||||
faceURL(character, thumbnail = false) {
|
||||
return `/img/${character}/` + (thumbnail ? 'face_thumb' : 'face') + '.jpeg'
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
<template>
|
||||
<span>
|
||||
<template v-for="(constituent, index) in texts">
|
||||
<router-link class="speaker-link" v-if="constituent.route" :key="index" :to="constituent.route">
|
||||
{{ constituent.text }}
|
||||
</router-link>
|
||||
<span class="speaker-bg" v-else :key="index">{{ constituent }}</span>
|
||||
</template>
|
||||
</span>
|
||||
<span>
|
||||
<template v-for="(constituent, index) in texts">
|
||||
<RouterLink
|
||||
class="speaker-link"
|
||||
v-if="constituent.route"
|
||||
:key="index"
|
||||
:to="constituent.route"
|
||||
>
|
||||
{{ constituent.text }}
|
||||
</RouterLink>
|
||||
<span class="speaker-bg" v-else :key="'plain-' + index">{{ constituent }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DynamicSpeaker",
|
||||
props: {
|
||||
text: {type: String, required: true},
|
||||
characters: {type: Object, required: true}
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DynamicSpeaker',
|
||||
|
||||
props: {
|
||||
text: { type: String, required: true },
|
||||
characters: { type: Object, required: true },
|
||||
},
|
||||
|
||||
computed: {
|
||||
texts() {
|
||||
return this.text.split(/({[^}]+})/).map((item) => {
|
||||
const id = item.substring(1, item.length - 1)
|
||||
if (item.startsWith('{'))
|
||||
return {
|
||||
text: this.characters[id],
|
||||
route: {
|
||||
name: 'Character',
|
||||
params: { character: id },
|
||||
},
|
||||
}
|
||||
else return item
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
texts() {
|
||||
return this.text.split(/({[^}]+})/).map((item) => {
|
||||
const id = item.substring(1, item.length - 1)
|
||||
if (item.startsWith('{'))
|
||||
return {
|
||||
text: this.characters[id],
|
||||
route: {
|
||||
name: 'Character', params: {character: id}
|
||||
}
|
||||
}
|
||||
else
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
+127
-117
@@ -1,135 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb v-if="ready" :items="breadcrumbs" />
|
||||
<b-card v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%;" />
|
||||
</b-card>
|
||||
<b-card class="mb-4">
|
||||
<template v-if="ready">
|
||||
<h3 class="card-title">
|
||||
"{{ episode.title }}"
|
||||
</h3>
|
||||
<span>{{ episode.description }}</span>
|
||||
<CharacterBadges v-if="episode && episode.characters" :characters="episode.characters" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Skeleton style="width: 30%;" />
|
||||
<Skeleton style="width: 70%; height: 60%;" />
|
||||
<Skeleton style="width: 45%; height: 60%;" />
|
||||
<Skeleton style="width: 69%; height: 40%;" />
|
||||
</template>
|
||||
</b-card>
|
||||
<div v-if="ready">
|
||||
<b-card
|
||||
v-for="(scene, sceneIndex) in episode.scenes" :key="`scene-${sceneIndex}`"
|
||||
class="mb-1" body-class="p-0"
|
||||
>
|
||||
<b-card-text class="my-2">
|
||||
<QuoteList :quotes="scene.quotes" :scene-index="sceneIndex" />
|
||||
<span
|
||||
v-if="scene.deleted" class="mt-n2 mb-4 text-muted deleted-scene pl-2"
|
||||
:footer="`Deleted Scene ${scene.deleted}`"
|
||||
>
|
||||
Deleted Scene {{ scene.deleted }}
|
||||
</span>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</div>
|
||||
<div>
|
||||
<BBreadcrumb v-if="ready" :items="breadcrumbs" />
|
||||
<BCard v-else class="breadcrumb-skeleton mb-3">
|
||||
<Skeleton style="width: 40%" />
|
||||
</BCard>
|
||||
<BCard class="mb-4">
|
||||
<template v-if="ready">
|
||||
<h3 class="card-title">"{{ episode.title }}"</h3>
|
||||
<span>{{ episode.description }}</span>
|
||||
<CharacterBadges v-if="episode && episode.characters" :characters="episode.characters" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Skeleton style="width: 30%" />
|
||||
<Skeleton style="width: 70%; height: 60%" />
|
||||
<Skeleton style="width: 45%; height: 60%" />
|
||||
<Skeleton style="width: 69%; height: 40%" />
|
||||
</template>
|
||||
</BCard>
|
||||
<div v-if="ready">
|
||||
<BCard
|
||||
v-for="(scene, sceneIndex) in episode.scenes"
|
||||
:key="`scene-${sceneIndex}`"
|
||||
class="mb-1"
|
||||
body-class="p-0"
|
||||
>
|
||||
<BCardText class="my-2">
|
||||
<QuoteList :quotes="scene.quotes" :scene-index="sceneIndex" />
|
||||
<span
|
||||
v-if="scene.deleted"
|
||||
class="mt-n2 mb-4 text-muted deleted-scene pl-2"
|
||||
:footer="`Deleted Scene ${scene.deleted}`"
|
||||
>
|
||||
Deleted Scene {{ scene.deleted }}
|
||||
</span>
|
||||
</BCardText>
|
||||
</BCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.card-title {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 500;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.deleted-scene {
|
||||
font-size: 0.75em;
|
||||
line-height: 12px;
|
||||
font-size: 0.75em;
|
||||
line-height: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import QuoteList from "./QuoteList.vue";
|
||||
import CharacterBadges from "./CharacterBadges.vue";
|
||||
import Skeleton from './Skeleton.vue';
|
||||
import {types} from "@/mutation_types";
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
|
||||
export default {
|
||||
name: "Episode",
|
||||
components: {
|
||||
QuoteList,
|
||||
CharacterBadges,
|
||||
Skeleton,
|
||||
import QuoteList from '@/components/QuoteList.vue'
|
||||
import CharacterBadges from '@/components/CharacterBadges.vue'
|
||||
import Skeleton from '@/components/Skeleton.vue'
|
||||
import { BBreadcrumb } from 'bootstrap-vue-next'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EpisodeComponent',
|
||||
|
||||
components: {
|
||||
QuoteList,
|
||||
CharacterBadges,
|
||||
Skeleton,
|
||||
BBreadcrumb,
|
||||
},
|
||||
|
||||
computed: {
|
||||
episode() {
|
||||
return this.$store.getters.getEpisode(this.params.season, this.params.episode)
|
||||
},
|
||||
computed: {
|
||||
episode() {
|
||||
return this.$store.getters.getEpisode(this.params.season, this.params.episode)
|
||||
},
|
||||
// Shorthand - literally useless, why does everything to have such long prefixes in dot notation
|
||||
params() {
|
||||
return this.$route.params
|
||||
},
|
||||
ready() {
|
||||
return this.$store.getters.isFetched(this.params.season, this.params.episode)
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {
|
||||
name: 'Home'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
to: {
|
||||
name: 'Season',
|
||||
season: this.$route.params.season
|
||||
}
|
||||
},
|
||||
{
|
||||
text: `Episode ${this.$route.params.episode}`,
|
||||
to: {
|
||||
name: 'Episode',
|
||||
season: this.$route.params.season,
|
||||
episode: this.$route.params.episode
|
||||
},
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
// Shorthand - literally useless, why does everything to have such long prefixes in dot notation
|
||||
params() {
|
||||
return this.$route.params
|
||||
},
|
||||
watch: {
|
||||
// When route changes, fetch data for current Episode route
|
||||
$route() {
|
||||
this.$nextTick(() => {
|
||||
this.fetch();
|
||||
ready() {
|
||||
return this.$store.getters.isFetched(this.params.season, this.params.episode)
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
to: {
|
||||
name: 'Season',
|
||||
season: this.$route.params.season,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `Episode ${this.$route.params.episode}`,
|
||||
to: {
|
||||
name: 'Episode',
|
||||
season: this.$route.params.season,
|
||||
episode: this.$route.params.episode,
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// When route changes, fetch data for current Episode route
|
||||
$route() {
|
||||
nextTick(() => {
|
||||
this.fetch()
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
// When page loads directly on this Episode initially, fetch data
|
||||
this.fetch()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetch() {
|
||||
// Fetch the episode, then scroll - already fetched episode should scroll immediately
|
||||
this.$store
|
||||
.dispatch(types.FETCH_EPISODE, { season: this.params.season, episode: this.params.episode })
|
||||
.then(() => {
|
||||
// Force update, as for some reason it doesn't update naturally. I hate it too.
|
||||
this.$forceUpdate()
|
||||
|
||||
// Scroll down to quote
|
||||
if (this.$route.hash) {
|
||||
nextTick(() => {
|
||||
const section = document.getElementById(this.$route.hash.substring(1))
|
||||
this.$scrollTo(section, 500, { easing: 'ease-in' })
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
created() {
|
||||
// When page loads directly on this Episode initially, fetch data
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
async fetch() {
|
||||
// Fetch the episode, then scroll - already fetched episode should scroll immediately
|
||||
this.$store.dispatch(types.FETCH_EPISODE, {season: this.params.season, episode: this.params.episode})
|
||||
.then(() => {
|
||||
// Force update, as for some reason it doesn't update naturally. I hate it too.
|
||||
this.$forceUpdate()
|
||||
|
||||
// Scroll down to quote
|
||||
if (this.$route.hash) {
|
||||
this.$nextTick(() => {
|
||||
const section = document.getElementById(this.$route.hash.substring(1));
|
||||
this.$scrollTo(section, 500, {easing: "ease-in"});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
+90
-80
@@ -1,99 +1,109 @@
|
||||
<template>
|
||||
<div class="outer-footer">
|
||||
<footer class="inner-footer">
|
||||
<b-container>
|
||||
<b-row 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>
|
||||
</b-row>
|
||||
<p v-if="buildTimeString !== null" class="build-time" :title="buildISOString">
|
||||
built on {{ buildTimeString }}
|
||||
</p>
|
||||
</b-container>
|
||||
</footer>
|
||||
</div>
|
||||
<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>
|
||||
export default {
|
||||
name: "Footer",
|
||||
props: {
|
||||
buildMoment: {type: Object, default: null}
|
||||
<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')
|
||||
},
|
||||
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/${process.env.VUE_APP_GIT_HASH}`
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
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;
|
||||
height: 100%;
|
||||
color: #6d6d6d;
|
||||
|
||||
.build-time {
|
||||
text-align: center;
|
||||
padding-top: 0.7em;
|
||||
opacity: 0.3;
|
||||
font-size: 0.85em;
|
||||
margin-bottom: 0;
|
||||
li {
|
||||
&:not(:last-child)::after {
|
||||
padding: 0 0.6em;
|
||||
content: '|';
|
||||
}
|
||||
|
||||
display: inline;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
display: table;
|
||||
margin: 0 auto;
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
opacity: 0.6;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+69
-64
@@ -1,71 +1,76 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<template v-if="ready">
|
||||
<h4>
|
||||
The Office Quotes
|
||||
</h4>
|
||||
<b-card-text>
|
||||
A Vue.js application serving you 54,000+ quotes from your
|
||||
favorite show - The Office.
|
||||
<br>
|
||||
Click on a Season and Episode on the left-hand sidebar to view quotes.
|
||||
Search for quotes with the instant searchbox.
|
||||
<br>
|
||||
This site is going through a big update & re-model, so the homepage isn't quite ready.
|
||||
However, as of the time of writing this, most everything else is setup.
|
||||
<hr>
|
||||
<p style="text-align: center">
|
||||
Check out the <router-link :to="{'name': 'About'}">
|
||||
about page
|
||||
</router-link> for more info on what this website is.
|
||||
</p>
|
||||
</b-card-text>
|
||||
</template>
|
||||
<b-card-text v-else>
|
||||
<Skeleton style="width: 45%" />
|
||||
<Skeleton style="width: 75%" />
|
||||
<Skeleton style="width: 60%" />
|
||||
<Skeleton style="width: 60%" />
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<BCard>
|
||||
<template v-if="ready">
|
||||
<h4>The Office Quotes</h4>
|
||||
<BCardText>
|
||||
A Vue.js application serving you 54,000+ quotes from your favorite show - The Office.
|
||||
<br />
|
||||
Click on a Season and Episode on the left-hand sidebar to view quotes. Search for quotes
|
||||
with the instant searchbox.
|
||||
<br />
|
||||
This site is going through a big update & re-model, so the homepage isn't quite ready.
|
||||
However, as of the time of writing this, most everything else is setup.
|
||||
<hr />
|
||||
<p style="text-align: center">
|
||||
Check out the <RouterLink :to="{ name: 'About' }"> about page </RouterLink> for more info
|
||||
on what this website is.
|
||||
</p>
|
||||
</BCardText>
|
||||
</template>
|
||||
<BCardText v-else>
|
||||
<Skeleton style="width: 45%" />
|
||||
<Skeleton style="width: 75%" />
|
||||
<Skeleton style="width: 60%" />
|
||||
<Skeleton style="width: 60%" />
|
||||
</BCardText>
|
||||
</BCard>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Skeleton from './Skeleton.vue';
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
Skeleton
|
||||
import axios from 'axios'
|
||||
import Skeleton from './Skeleton.vue'
|
||||
import { BCardText, BCard } from 'bootstrap-vue-next'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomeComponent',
|
||||
|
||||
components: {
|
||||
Skeleton,
|
||||
BCardText,
|
||||
BCard,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
stats: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
ready() {
|
||||
return true
|
||||
// return this.stats != null;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stats: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
// this.getStats();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getStats() {
|
||||
const path = `${import.meta.env.VUE_APP_API_URL}/api/stats/`
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.stats = res.data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return true;
|
||||
// return this.stats != null;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// this.getStats();
|
||||
},
|
||||
methods: {
|
||||
getStats() {
|
||||
const path = `${process.env.VUE_APP_API_URL}/api/stats/`;
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.stats = res.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
<div class="image-skeleton" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ImageSkeleton"
|
||||
}
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageSkeleton",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/scss/_variables.scss" as *;
|
||||
|
||||
.image-skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $grey-4;
|
||||
background-color: $gray-400;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -1,76 +1,91 @@
|
||||
<template>
|
||||
<table class="quote-list px-3 w-100">
|
||||
<tr
|
||||
v-for="(quote, index) in quotes"
|
||||
:id="`${sceneIndex}-${index}`"
|
||||
:key="`quote-${index}`"
|
||||
:class="
|
||||
$route.hash !== null &&
|
||||
$route.hash.substring(1) === `${sceneIndex}-${index}`
|
||||
? 'highlight'
|
||||
: ''
|
||||
"
|
||||
<table class="quote-list px-3 w-100">
|
||||
<tr
|
||||
v-for="(quote, index) in quotes"
|
||||
:id="`${sceneIndex}-${index}`"
|
||||
:key="`quote-${index}`"
|
||||
:class="
|
||||
$route.hash !== null && $route.hash.substring(1) === `${sceneIndex}-${index}`
|
||||
? 'highlight'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<td v-if="quote.speaker" class="quote-speaker pl-3">
|
||||
<DynamicSpeaker
|
||||
v-if="quote.isAnnotated"
|
||||
:text="quote.speaker"
|
||||
:characters="quote.characters"
|
||||
class="my-3"
|
||||
/>
|
||||
<RouterLink
|
||||
v-else
|
||||
:to="{ name: 'Character', params: { character: quote.character } }"
|
||||
class="speaker-link"
|
||||
>
|
||||
<td v-if="quote.speaker" class="quote-speaker pl-3">
|
||||
<DynamicSpeaker v-if="quote.isAnnotated" :text="quote.speaker" :characters="quote.characters" class="my-3" />
|
||||
<router-link v-else :to="{name: 'Character', params: {character: quote.character}}" class="speaker-link">
|
||||
{{ quote.speaker }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3" v-html="transform(quote.text)" />
|
||||
<td class="px-1 pl-2">
|
||||
<a :href="quote_link(index)" class="no-link" @click="copy(index)">
|
||||
<b-icon icon="link45deg" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{ quote.speaker }}
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3" v-html="transform(quote.text)" />
|
||||
<td class="px-1 pl-2">
|
||||
<a :href="quote_link(index)" class="no-link" @click="copy(index)">
|
||||
<!-- <b-icon icon="link45deg" /> -->
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
.speaker-bg {
|
||||
color: $grey-8;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.speaker-link {
|
||||
&, &:hover {
|
||||
color: $grey-10;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
&,
|
||||
&:hover {
|
||||
color: $gray-100;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import DynamicSpeaker from "@/components/DynamicSpeaker";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DynamicSpeaker
|
||||
import DynamicSpeaker from '@/components/DynamicSpeaker.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DynamicSpeaker,
|
||||
},
|
||||
|
||||
props: {
|
||||
sceneIndex: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
props: {
|
||||
sceneIndex: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
quotes: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
quotes: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
methods: {
|
||||
transform(quoteText) {
|
||||
if (quoteText.includes("[")) {
|
||||
return quoteText.replace(/\[([^\]]+)]/g, ' <i>[$1]</i> ')
|
||||
}
|
||||
return quoteText
|
||||
},
|
||||
quote_link(quoteIndex) {
|
||||
return `/${this.$route.params.season}/${this.$route.params.episode}#${this.sceneIndex}-${quoteIndex}`;
|
||||
},
|
||||
copy(quoteIndex) {
|
||||
this.$copyText(process.env.VUE_APP_BASE_URL + this.quote_link(quoteIndex))
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
transform(quoteText) {
|
||||
if (quoteText.includes('[')) {
|
||||
return quoteText.replace(/\[([^\]]+)]/g, ' <i>[$1]</i> ')
|
||||
}
|
||||
return quoteText
|
||||
},
|
||||
};
|
||||
quote_link(quoteIndex) {
|
||||
return `/${this.$route.params.season}/${this.$route.params.episode}#${this.sceneIndex}-${quoteIndex}`
|
||||
},
|
||||
copy(quoteIndex) {
|
||||
this.$copyText(import.meta.env.VUE_APP_BASE_URL + this.quote_link(quoteIndex))
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
+127
-137
@@ -1,166 +1,156 @@
|
||||
<template>
|
||||
<b-card
|
||||
class="mb-1" body-class="p-0 expandable-result"
|
||||
footer-class="my-1"
|
||||
:class="[expanded ? 'expanded' : '']"
|
||||
@mouseover="hoverOn"
|
||||
@mouseleave="hoverOff"
|
||||
@click="toggleExpansion"
|
||||
>
|
||||
<b-card-text class="mu-2 py-1 mb-1">
|
||||
<table v-if="expanded" class="quote-list px-3 py-1 w-100">
|
||||
<tr
|
||||
v-for="(quote, index) in above"
|
||||
:key="`quote-a-${index}`"
|
||||
class="secondary"
|
||||
>
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="quote-speaker my-3 pl-3"
|
||||
v-html="item._highlightResult.speaker.value"
|
||||
/>
|
||||
<td
|
||||
class="quote-text w-100 pr-3"
|
||||
v-html="item._highlightResult.text.value"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(quote, index) in below"
|
||||
:key="`quote-b-${index}`"
|
||||
class="secondary"
|
||||
>
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-else class="quote-list px-3 py-1 w-100">
|
||||
<tr>
|
||||
<td
|
||||
class="quote-speaker my-3 pl-3"
|
||||
v-html="item._highlightResult.speaker.value"
|
||||
/>
|
||||
<td
|
||||
class="quote-text w-100 pr-3"
|
||||
v-html="item._highlightResult.text.value"
|
||||
/>
|
||||
</tr>
|
||||
</table>
|
||||
<router-link
|
||||
v-if="expanded" class="no-link search-result-link w-100 text-muted mb-2 ml-2"
|
||||
:to="{name: 'Episode', params: { season: item.season, episode: item.episode_rel },
|
||||
hash: `#${item.section_rel - 1}-${item.quote_rel - 1}`, }"
|
||||
>
|
||||
Season {{ item.season }} Episode {{ item.episode_rel }} Scene
|
||||
{{ item.section_rel }}
|
||||
</router-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<BCard
|
||||
class="mb-1"
|
||||
body-class="p-0 expandable-result"
|
||||
footer-class="my-1"
|
||||
:class="[expanded ? 'expanded' : '']"
|
||||
@mouseover="hoverOn"
|
||||
@mouseleave="hoverOff"
|
||||
@click="toggleExpansion"
|
||||
>
|
||||
<BCardText class="mu-2 py-1 mb-1">
|
||||
<table v-if="expanded" class="quote-list px-3 py-1 w-100">
|
||||
<tr v-for="(quote, index) in above" :key="`quote-a-${index}`" class="secondary">
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="quote-speaker my-3 pl-3" v-html="item._highlightResult.speaker.value" />
|
||||
<td class="quote-text w-100 pr-3" v-html="item._highlightResult.text.value" />
|
||||
</tr>
|
||||
<tr v-for="(quote, index) in below" :key="`quote-b-${index}`" class="secondary">
|
||||
<td class="quote-speaker my-3 pl-3">
|
||||
<div>{{ quote.speaker }}</div>
|
||||
</td>
|
||||
<td class="quote-text w-100 pr-3">
|
||||
<div>{{ quote.text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-else class="quote-list px-3 py-1 w-100">
|
||||
<tr>
|
||||
<td class="quote-speaker my-3 pl-3" v-html="item._highlightResult.speaker.value" />
|
||||
<td class="quote-text w-100 pr-3" v-html="item._highlightResult.text.value" />
|
||||
</tr>
|
||||
</table>
|
||||
<RouterLink
|
||||
v-if="expanded"
|
||||
class="no-link search-result-link w-100 text-muted mb-2 ml-2"
|
||||
:to="{
|
||||
name: 'Episode',
|
||||
params: { season: item.season, episode: item.episode_rel },
|
||||
hash: `#${item.section_rel - 1}-${item.quote_rel - 1}`,
|
||||
}"
|
||||
>
|
||||
Season {{ item.season }} Episode {{ item.episode_rel }} Scene
|
||||
{{ item.section_rel }}
|
||||
</RouterLink>
|
||||
</BCardText>
|
||||
</BCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
.expandable-result {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-result-link {
|
||||
white-space: nowrap;
|
||||
font-size: 0.75em !important;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75em !important;
|
||||
}
|
||||
|
||||
.quote-list > tr {
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-4;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
white-space: normal;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.quote-speaker {
|
||||
min-width: 75px;
|
||||
padding-right: 1em;
|
||||
font-weight: 600;
|
||||
vertical-align: text-top;
|
||||
text-align: right;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
min-width: 75px;
|
||||
padding-right: 1em;
|
||||
font-weight: 600;
|
||||
vertical-align: text-top;
|
||||
text-align: right;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
props: ["item"],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
fetching: false,
|
||||
above: null,
|
||||
below: null,
|
||||
timeoutID: null
|
||||
};
|
||||
import axios from 'axios'
|
||||
|
||||
export default defineComponent({
|
||||
props: ['item'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
fetching: false,
|
||||
above: null,
|
||||
below: null,
|
||||
timeoutID: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fetched() {
|
||||
return this.above !== null || this.below !== null
|
||||
},
|
||||
computed: {
|
||||
fetched() {
|
||||
return this.above !== null || this.below !== null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExpansion() {
|
||||
this.expanded = !this.expanded
|
||||
// if first time expanding, fetch quotes
|
||||
if (!this.fetchQuotes()) {
|
||||
this.hasExpanded = true
|
||||
// this.fetchQuotes();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleExpansion() {
|
||||
this.expanded = !this.expanded;
|
||||
// if first time expanding, fetch quotes
|
||||
if (!this.fetchQuotes()) {
|
||||
this.hasExpanded = true;
|
||||
// this.fetchQuotes();
|
||||
}
|
||||
},
|
||||
hoverFetch() {
|
||||
if (!this.fetched && !this.fetching) {
|
||||
this.fetching = true;
|
||||
this.fetchQuotes();
|
||||
this.fetching = false;
|
||||
}
|
||||
},
|
||||
hoverOn() {
|
||||
// Schedule a fetching event
|
||||
// this.timeoutID = setTimeout(this.hoverFetch, 300);
|
||||
},
|
||||
hoverOff() {
|
||||
// Hover is off. Unschedule event if it has not already fetched.
|
||||
if (this.timeoutID !== null)
|
||||
clearTimeout(this.timeoutID);
|
||||
},
|
||||
fetchQuotes() {
|
||||
const path = `/api/surrounding?season=${this.item.season}&episode=${this.item.episode_rel}&scene=${this.item.section_rel}"e=${this.item.quote_rel}`;
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.above = res.data.above;
|
||||
this.below = res.data.below;
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
hoverFetch() {
|
||||
if (!this.fetched && !this.fetching) {
|
||||
this.fetching = true
|
||||
this.fetchQuotes()
|
||||
this.fetching = false
|
||||
}
|
||||
},
|
||||
};
|
||||
hoverOn() {
|
||||
// Schedule a fetching event
|
||||
// this.timeoutID = setTimeout(this.hoverFetch, 300);
|
||||
},
|
||||
hoverOff() {
|
||||
// Hover is off. Unschedule event if it has not already fetched.
|
||||
if (this.timeoutID !== null) clearTimeout(this.timeoutID)
|
||||
},
|
||||
fetchQuotes() {
|
||||
const path = `/api/surrounding?season=${this.item.season}&episode=${this.item.episode_rel}&scene=${this.item.section_rel}"e=${this.item.quote_rel}`
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.above = res.data.above
|
||||
this.below = res.data.below
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<ais-hits>
|
||||
<div slot-scope="{ items }">
|
||||
<template v-slot="{ items }">
|
||||
<div>
|
||||
<SearchResult v-for="item in items" :key="item.objectID" :item="item" />
|
||||
</div>
|
||||
</template>
|
||||
</ais-hits>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchResult from "./SearchResult.vue";
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default {
|
||||
name: "SearchResults",
|
||||
components: {
|
||||
SearchResult,
|
||||
},
|
||||
};
|
||||
import SearchResult from "@/components/SearchResult.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
|
||||
components: {
|
||||
SearchResult,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
+94
-77
@@ -1,88 +1,105 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-breadcrumb :items="breadcrumbs" />
|
||||
<b-card v-if="ready">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="episode in season.episodes" :key="episode.episodeNumber">
|
||||
<b-row align-v="start" align-content="start">
|
||||
<b-col cols="5" md="4" lg="4" xl="3">
|
||||
<b-img-lazy
|
||||
fluid-grow class="px-2"
|
||||
width="200" height="200"
|
||||
blank-width="200" blank-height="200"
|
||||
:src="getUrl(episode.episodeNumber, episode.seasonNumber)"
|
||||
:blank-src="getUrl(episode.episodeNumber, episode.seasonNumber, true)"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<h4>
|
||||
{{ episode.title }}
|
||||
<router-link
|
||||
class="no-link"
|
||||
:to="getEpisodeRoute(episode.seasonNumber, episode.episodeNumber)"
|
||||
>
|
||||
<b-icon class="h6" icon="caret-right-fill" />
|
||||
</router-link>
|
||||
</h4>
|
||||
<p class="pl-3">
|
||||
{{ episode.description }}
|
||||
</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</div>
|
||||
<div>
|
||||
<BBreadcrumb :items="breadcrumbs" />
|
||||
<BCard v-if="ready">
|
||||
<BListGroup>
|
||||
<BListGroupItem v-for="episode in season.episodes" :key="episode.episodeNumber">
|
||||
<BRow align-v="start" align-content="start">
|
||||
<BCol cols="5" md="4" lg="4" xl="3">
|
||||
<BImg
|
||||
fluid-grow
|
||||
class="px-2"
|
||||
width="200"
|
||||
height="200"
|
||||
blank-width="200"
|
||||
blank-height="200"
|
||||
:src="getUrl(episode.episodeNumber, episode.seasonNumber)"
|
||||
:blank-src="getUrl(episode.episodeNumber, episode.seasonNumber, true)"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<h4>
|
||||
{{ episode.title }}
|
||||
<RouterLink
|
||||
class="no-link"
|
||||
:to="getEpisodeRoute(episode.seasonNumber, episode.episodeNumber)"
|
||||
>
|
||||
<b-icon class="h6" icon="caret-right-fill" />
|
||||
</RouterLink>
|
||||
</h4>
|
||||
<p class="pl-3">
|
||||
{{ episode.description }}
|
||||
</p>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BListGroupItem>
|
||||
</BListGroup>
|
||||
</BCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use 'sass:color';
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
h4 {
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
&:hover {
|
||||
color: darken(#007fe0, 10%);
|
||||
}
|
||||
.b-icon {
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
color: #007fe0;
|
||||
&:hover {
|
||||
color: color.adjust(#007fe0, $lightness: -10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.getters.checkPreloaded('episodes');
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: {name: 'Home'}
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
},
|
||||
season() {
|
||||
return this.$store.state.quoteData[this.$route.params.season - 1];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getEpisodeRoute(s, e) {
|
||||
return {name: 'Episode', params: {season: s, episode: e}}
|
||||
},
|
||||
getUrl(episode, season, thumbnail = false) {
|
||||
episode = episode.toString().padStart(2, "0")
|
||||
season = season.toString().padStart(2, "0")
|
||||
const filename = thumbnail ? 'thumbnail.jpeg' : 'full.jpeg'
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { BBreadcrumb, BImg } from 'bootstrap-vue-next'
|
||||
|
||||
return `/img/${season}/${episode}/${filename}`
|
||||
}
|
||||
}
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'SeasonComponent',
|
||||
|
||||
components: {
|
||||
BBreadcrumb,
|
||||
BImg,
|
||||
},
|
||||
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.getters.checkPreloaded('episodes')
|
||||
},
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
to: { name: 'Home' },
|
||||
},
|
||||
{
|
||||
text: `Season ${this.$route.params.season}`,
|
||||
active: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
season() {
|
||||
return this.$store.state.quoteData[this.$route.params.season - 1]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getEpisodeRoute(s, e) {
|
||||
return { name: 'Episode', params: { season: s, episode: e } }
|
||||
},
|
||||
getUrl(episode, season, thumbnail = false) {
|
||||
episode = episode.toString().padStart(2, '0')
|
||||
season = season.toString().padStart(2, '0')
|
||||
const filename = thumbnail ? 'thumbnail.jpeg' : 'full.jpeg'
|
||||
|
||||
return `/img/${season}/${episode}/${filename}`
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,71 +1,61 @@
|
||||
<template>
|
||||
<div class="accordion" role="tablist">
|
||||
<b-card v-for="season in seasons" :key="season.season_id" class="season-item">
|
||||
<b-card-header v-b-toggle="'accordion-' + season.season_id" header-tag="header" role="tab">
|
||||
<a v-if="isPreloaded" class="no-link align-items-center justify-content-between d-flex">
|
||||
<h5 class="mb-0 pu-0 mu-0 season-title">
|
||||
Season {{ season.season_id }}
|
||||
</h5>
|
||||
<b-icon class="" icon="chevron-down" />
|
||||
</a>
|
||||
<Skeleton v-else />
|
||||
</b-card-header>
|
||||
<b-collapse :id="'accordion-' + season.season_id" accordion="accordion-season-list">
|
||||
<b-card-body class="h-100 px-0">
|
||||
<b-list-group>
|
||||
<template v-for="(episode, index) in seasons[season.season_id - 1].episodes">
|
||||
<template v-if="isPreloaded">
|
||||
<SeasonListItem
|
||||
:key="`rl-${index}`"
|
||||
:episode-number="episode.episodeNumber"
|
||||
:season-number="episode.seasonNumber"
|
||||
:title="episode.title"
|
||||
/>
|
||||
<b-popover
|
||||
:key="`bpop-${episode.episode_id}`" triggers="hover"
|
||||
placement="right" delay="25"
|
||||
:target="`s-${season.season_id}-ep-${episode.episode_id}`"
|
||||
>
|
||||
<template v-slot:title>
|
||||
{{ episode.title }}
|
||||
</template>
|
||||
{{ episode.description }}
|
||||
</b-popover>
|
||||
</template>
|
||||
<b-list-group-item v-else :key="index" class="no-link episode-item">
|
||||
<Skeleton />
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
</b-list-group>
|
||||
</b-card-body>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import SeasonListItem from '@/components/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';
|
||||
|
||||
<script>
|
||||
import Skeleton from '@/components/Skeleton';
|
||||
import SeasonListItem from "@/components/SeasonListItem";
|
||||
import {types} from "@/mutation_types";
|
||||
import useStore from '@/store';
|
||||
|
||||
export default {
|
||||
name: "SeasonList",
|
||||
components: {
|
||||
Skeleton,
|
||||
SeasonListItem
|
||||
},
|
||||
computed: {
|
||||
seasons() {
|
||||
return this.$store.state.quoteData;
|
||||
},
|
||||
// if SeasonList episode data (titles/descriptions) is loaded and ready
|
||||
isPreloaded() {
|
||||
return this.$store.getters.checkPreloaded('episodes');
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch(types.PRELOAD_EPISODES)
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
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>
|
||||
|
||||
@@ -1,61 +1,58 @@
|
||||
<template>
|
||||
<b-list-group-item
|
||||
v-if="dataAvailable"
|
||||
:id="`s-${seasonNumber}-ep-${episodeNumber}`"
|
||||
:to="{name: 'Episode', params: { season: seasonNumber, episode: episodeNumber }, }"
|
||||
class="no-link episode-item" @mouseover="hoverOn"
|
||||
@mouseleave="hoverOff"
|
||||
>
|
||||
Episode {{ episodeNumber }} - "{{ title }}"
|
||||
</b-list-group-item>
|
||||
<b-list-group-item v-else>
|
||||
<Skeleton style="width: 90%" />
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
<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';
|
||||
|
||||
<script>
|
||||
import {types} from "@/mutation_types";
|
||||
import Skeleton from "@/components/Skeleton";
|
||||
const store = useStore();
|
||||
|
||||
export default {
|
||||
name: "SeasonListItem",
|
||||
components: {
|
||||
Skeleton
|
||||
},
|
||||
props: {
|
||||
episodeNumber: {type: Number, default: null, required: false},
|
||||
seasonNumber: {type: Number, default: null, required: false},
|
||||
title: {type: String, default: null, required: false}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timeoutID: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataAvailable() {
|
||||
return this.episodeNumber !== null &&
|
||||
this.seasonNumber !== null &&
|
||||
this.title !== null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hoverFetch() {
|
||||
this.$store.dispatch(types.FETCH_EPISODE, {season: this.seasonNumber, episode: this.episodeNumber})
|
||||
},
|
||||
hoverOn() {
|
||||
this.timeoutID = setTimeout(this.hoverFetch, 800);
|
||||
},
|
||||
hoverOff() {
|
||||
if (this.timeoutID !== null) {
|
||||
clearTimeout(this.timeoutID)
|
||||
this.timeoutID = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
<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>
|
||||
<style scoped></style>
|
||||
|
||||
+93
-83
@@ -1,104 +1,114 @@
|
||||
<template>
|
||||
<div class="outer-skeleton">
|
||||
<div
|
||||
class="skeleton"
|
||||
:class="[animated ? undefined : 'no-animate']"
|
||||
:style="[style, innerStyle]"
|
||||
/>
|
||||
</div>
|
||||
<div class="outer-skeleton">
|
||||
<div
|
||||
class="skeleton"
|
||||
:class="[animated ? undefined : 'no-animate']"
|
||||
:style="[style, innerStyle]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.breadcrumb-skeleton {
|
||||
height: 48px;
|
||||
@use '@/scss/_variables.scss' as *;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.breadcrumb-skeleton {
|
||||
height: 48px;
|
||||
|
||||
& > .card-body {
|
||||
padding: 0 0 0 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-skeleton {
|
||||
padding: 0.35em 0.3em 0.35em 0.35em;
|
||||
padding: 0.35em 0.3em 0.35em 0.35em;
|
||||
|
||||
.breadcrumb-skeleton > {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
background-size: 200px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(90deg, var(--secondary-color, $grey-4), var(--primary-color, $grey-6), var(--secondary-color, $grey-4));
|
||||
background-color: var(--secondary-color, $grey-4);
|
||||
animation: 1.25s ease-in-out 0s infinite normal none running SkeletonLoading;
|
||||
border-radius: var(--border-radius, 3px);
|
||||
|
||||
&.no-animate {
|
||||
animation: none;
|
||||
}
|
||||
// .breadcrumb-skeleton > {
|
||||
// display: inline-block;
|
||||
// }
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
background-size: 200px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--secondary-color, $gray-100),
|
||||
var(--primary-color, $gray-100),
|
||||
var(--secondary-color, $gray-100)
|
||||
);
|
||||
background-color: var(--secondary-color, $gray-100);
|
||||
animation: 1.25s ease-in-out 0s infinite normal none running SkeletonLoading;
|
||||
border-radius: var(--border-radius, 3px);
|
||||
|
||||
&.no-animate {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes SkeletonLoading {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes SkeletonLoading {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
innerStyle: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
innerClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
borderRadius: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
primaryColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
secondaryColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
innerStyle: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
'--primary-color': this.primaryColor,
|
||||
'--secondary-color': this.secondaryColor,
|
||||
'--border-radius': this.borderRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
innerClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
borderRadius: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
primaryColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
secondaryColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
'--primary-color': this.primaryColor,
|
||||
'--secondary-color': this.secondaryColor,
|
||||
'--border-radius': this.borderRadius,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccordionRoot,
|
||||
type AccordionRootEmits,
|
||||
type AccordionRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot data-slot="accordion" v-bind="forwarded">
|
||||
<slot />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AccordionContent, type AccordionContentProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
data-slot="accordion-content"
|
||||
v-bind="delegatedProps"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
>
|
||||
<div :class="cn('pt-0 pb-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem
|
||||
data-slot="accordion-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b last:border-b-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'reka-ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
data-slot="accordion-trigger"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Accordion } from './Accordion.vue'
|
||||
export { default as AccordionContent } from './AccordionContent.vue'
|
||||
export { default as AccordionItem } from './AccordionItem.vue'
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-display: 'American Typewriter', sans-serif;
|
||||
--font-roboto-slab: 'Roboto Slab', sans-serif;
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'American Typewriter';
|
||||
font-style: normal;
|
||||
font-weight: 200 700;
|
||||
font-display: swap;
|
||||
src: url('@/assets/american_typewriter.woff');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import '@/scss/main.scss';
|
||||
import Vue from "vue";
|
||||
import {BootstrapVue, BootstrapVueIcons} from "bootstrap-vue";
|
||||
import InstantSearch from "vue-instantsearch";
|
||||
import VueProgressiveImage from 'vue-progressive-image'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import VueScrollTo from "vue-scrollto";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
|
||||
|
||||
Vue.use(VueProgressiveImage)
|
||||
|
||||
Vue.use(VueScrollTo);
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(BootstrapVueIcons);
|
||||
Vue.use(InstantSearch);
|
||||
Vue.use(VueClipboard)
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
// Prevent invalid episodes, seasons and characters from being accessed
|
||||
router.beforeEach((to, from, next) => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (from.name !== null && to.name === "Character" && false) next(false);
|
||||
else next();
|
||||
});
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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');
|
||||
@@ -1,11 +0,0 @@
|
||||
export const types = {
|
||||
FETCH_EPISODE: 'FETCH_EPISODE',
|
||||
SET_EPISODE: 'SET_EPISODE',
|
||||
MERGE_EPISODE: 'MERGE_EPISODE',
|
||||
MERGE_EPISODES: 'MERGE_EPISODES',
|
||||
SET_PRELOADED: 'SET_PRELOADED',
|
||||
PRELOAD_CHARACTERS: 'PRELOAD_CHARACTERS',
|
||||
PRELOAD_EPISODES: 'PRELOAD',
|
||||
SET_CHARACTER: 'SET_CHARACTER',
|
||||
MERGE_CHARACTERS: 'MERGE_CHARACTERS',
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
import Home from "@/components/Home.vue";
|
||||
import Episode from "@/components/Episode.vue";
|
||||
import SearchResults from "@/components/SearchResults.vue";
|
||||
import Character from "@/components/Character.vue";
|
||||
import Season from "@/components/Season.vue";
|
||||
import Characters from "@/components/Characters";
|
||||
import About from "@/components/About";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
mode: "history",
|
||||
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/",
|
||||
name: "Season",
|
||||
component: Season,
|
||||
},
|
||||
{
|
||||
path: "/:season/:episode",
|
||||
name: "Episode",
|
||||
component: Episode,
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
},
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// https://router.vuejs.org/guide/advanced/scroll-behavior.html
|
||||
if (to.hash) {
|
||||
return {selector: to.hash};
|
||||
}
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import Home from '@/components/Home.vue'
|
||||
import Episode from '@/components/Episode.vue'
|
||||
import SearchResults from '@/components/SearchResults.vue'
|
||||
import Character from '@/components/Character.vue'
|
||||
import Season from '@/components/Season.vue'
|
||||
import Characters from '@/components/Characters.vue'
|
||||
import About from '@/components/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/',
|
||||
name: 'Season',
|
||||
component: Season,
|
||||
},
|
||||
{
|
||||
path: '/:season/: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
|
||||
+12
-12
@@ -1,13 +1,13 @@
|
||||
$grey-11: #dddddd;
|
||||
$grey-10: #cfcfcf;
|
||||
$grey-9: #b3b3b3;
|
||||
$grey-8: #a0a0a0;
|
||||
$grey-7: #565656;
|
||||
$grey-6: #3e3e3e;
|
||||
$grey-5: #292929;
|
||||
$grey-4: #242424;
|
||||
$grey-3: #191919;
|
||||
$grey-2: #131313;
|
||||
$grey-1: #0e0e0e;
|
||||
$grey-0: #070707;
|
||||
$gray-1100: #dddddd;
|
||||
$gray-1000: #cfcfcf;
|
||||
$gray-900: #b3b3b3;
|
||||
$gray-800: #a0a0a0;
|
||||
$gray-700: #565656;
|
||||
$gray-600: #3e3e3e;
|
||||
$gray-500: #292929;
|
||||
$gray-400: #242424;
|
||||
$gray-300: #191919;
|
||||
$gray-200: #131313;
|
||||
$gray-100: #0e0e0e;
|
||||
$gray-000: #070707;
|
||||
$highlight: #d2ca00;
|
||||
|
||||
+221
-270
@@ -1,322 +1,273 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap");
|
||||
// @use "sass:color";
|
||||
// @use "@/scss/_variables.scss" as theme;
|
||||
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 456px,
|
||||
md: 689px,
|
||||
lg: 900px,
|
||||
xl: 1450px
|
||||
);
|
||||
// @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap");
|
||||
// @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
|
||||
// @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap");
|
||||
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import '~bootstrap/scss/_mixins';
|
||||
@import '~bootstrap-vue/src/index.scss';
|
||||
|
||||
body {
|
||||
background-color: $grey-0;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
// $grid-breakpoints: (
|
||||
// xs: 0,
|
||||
// sm: 456px,
|
||||
// md: 689px,
|
||||
// lg: 900px,
|
||||
// xl: 1450px
|
||||
// );
|
||||
|
||||
.navbar {
|
||||
background-color: $grey-2;
|
||||
}
|
||||
// // @import '~bootstrap/scss/_mixins';
|
||||
|
||||
.navbar-brand {
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-size: 1.4em;
|
||||
color: $grey-11 !important;
|
||||
}
|
||||
// // Apply
|
||||
|
||||
.nav-link {
|
||||
color: $grey-9 !important;
|
||||
}
|
||||
// table.quote-list tr td:last-child {
|
||||
// height: 100%;
|
||||
|
||||
// Apply
|
||||
.card {
|
||||
color: $grey-9;
|
||||
background-color: $grey-2;
|
||||
border-bottom: 1px solid $grey-1;
|
||||
border-radius: 0;
|
||||
}
|
||||
// a {
|
||||
// height: 100%;
|
||||
// }
|
||||
|
||||
.quote-list > tr {
|
||||
white-space: nowrap;
|
||||
// svg {
|
||||
// font-size: 1.35em;
|
||||
// opacity: 0;
|
||||
// transition: opacity 0.1s ease-in;
|
||||
// }
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-4;
|
||||
}
|
||||
// table.quote-list tr:hover td:last-child svg {
|
||||
// opacity: 1;
|
||||
// }
|
||||
|
||||
&.highlight {
|
||||
background-color: $grey-5 !important;
|
||||
}
|
||||
}
|
||||
// // Make all season cards 'clickable'
|
||||
// .season-item > .card-body > .card-header {
|
||||
// cursor: pointer;
|
||||
// }
|
||||
|
||||
.quote-text {
|
||||
white-space: normal;
|
||||
}
|
||||
// // Make all chevron icons rotate 180 when clicked
|
||||
// .bi-chevron-down {
|
||||
// -moz-transition: all 0.25s ease-in-out;
|
||||
// -webkit-transition: all 0.25s ease-in-out;
|
||||
// transition: all 0.25s ease-in-out;
|
||||
// }
|
||||
|
||||
.quote-speaker {
|
||||
color: darken($grey-10, 1.75%);
|
||||
min-width: 100px;
|
||||
padding-right: 1em;
|
||||
font-weight: 600;
|
||||
vertical-align: text-top;
|
||||
text-align: right;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
// .not-collapsed > .bi-chevron-down {
|
||||
// transform: rotate(180deg);
|
||||
// -ms-transform: rotate(180deg);
|
||||
// -moz-transform: rotate(180deg);
|
||||
// -webkit-transform: rotate(180deg);
|
||||
// }
|
||||
|
||||
table.quote-list tr td:last-child {
|
||||
height: 100%;
|
||||
// // White popovers use white background on top left/right corners, this disables it.
|
||||
// .b-popover {
|
||||
// background: transparent;
|
||||
// border-color: theme.$gray-100 !important;
|
||||
// }
|
||||
|
||||
a {
|
||||
height: 100%;
|
||||
}
|
||||
// // Dark theme popover
|
||||
// .popover-header {
|
||||
// background-color: color.adjust(theme.$gray-200, $lightness: -2.1%);
|
||||
// border-color: theme.$gray-100;
|
||||
// color: theme.$gray-100;
|
||||
// }
|
||||
|
||||
svg {
|
||||
font-size: 1.35em;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease-in;
|
||||
}
|
||||
}
|
||||
// // Dark theme popover, arrow-right fix
|
||||
// .bs-popover-top > .arrow::after,
|
||||
// .bs-popover-auto[x-placement^="top"] > .arrow::after {
|
||||
// border-color: color.adjust(theme.$gray-300, $lightness: -2%) !important;
|
||||
// }
|
||||
|
||||
table.quote-list tr:hover td:last-child svg {
|
||||
opacity: 1;
|
||||
}
|
||||
// .bs-popover-bottom > .arrow::after,
|
||||
// .bs-popover-auto[x-placement^="bottom"] > .arrow::after {
|
||||
// border-bottom-color: color.adjust(theme.$gray-300, $lightness: -2%) !important;
|
||||
// }
|
||||
|
||||
// Make all season cards 'clickable'
|
||||
.season-item > .card-body > .card-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
// .bs-popover-left > .arrow::after,
|
||||
// .bs-popover-auto[x-placement^="left"] > .arrow::after {
|
||||
// border-left-color: color.adjust(theme.$gray-300, $lightness: -2%);
|
||||
// }
|
||||
|
||||
// Make all chevron icons rotate 180 when clicked
|
||||
.bi-chevron-down {
|
||||
-moz-transition: all 0.25s ease-in-out;
|
||||
-webkit-transition: all 0.25s ease-in-out;
|
||||
transition: all 0.25s ease-in-out;
|
||||
}
|
||||
// .bs-popover-right > .arrow::after,
|
||||
// .bs-popover-auto[x-placement^="right"] > .arrow::after {
|
||||
// border-right-color: color.adjust(theme.$gray-300, $lightness: -2%) !important;
|
||||
// }
|
||||
|
||||
.not-collapsed > .bi-chevron-down {
|
||||
transform: rotate(180deg);
|
||||
-ms-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-webkit-transform: rotate(180deg);
|
||||
}
|
||||
// .season-item .list-group-item {
|
||||
// background-color: theme.$gray-300;
|
||||
|
||||
// White popovers use white background on top left/right corners, this disables it.
|
||||
.b-popover {
|
||||
background: transparent;
|
||||
border-color: $grey-1 !important;
|
||||
}
|
||||
// &:first-child {
|
||||
// border-radius: 0;
|
||||
// }
|
||||
|
||||
// Dark theme popover
|
||||
.popover-header {
|
||||
background-color: darken($grey-2, 2.1%);
|
||||
border-color: $grey-1;
|
||||
color: $grey-11;
|
||||
}
|
||||
// &:hover {
|
||||
// background-color: color.adjust(theme.$gray-300, $lightness: 2.5%);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Dark theme popover, arrow-right fix
|
||||
.bs-popover-top > .arrow::after,
|
||||
.bs-popover-auto[x-placement^="top"] > .arrow::after {
|
||||
border-color: darken($grey-3, 2%) !important;
|
||||
}
|
||||
// // Dark theme popover body
|
||||
// .popover-body {
|
||||
// color: theme.$gray-800 !important;
|
||||
// background-color: color.adjust(theme.$gray-300, $lightness: -2%) !important;
|
||||
// }
|
||||
|
||||
.bs-popover-bottom > .arrow::after,
|
||||
.bs-popover-auto[x-placement^="bottom"] > .arrow::after {
|
||||
border-bottom-color: darken($grey-3, 2%) !important;
|
||||
}
|
||||
// .season-title {
|
||||
// color: theme.$gray-800;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
|
||||
.bs-popover-left > .arrow::after,
|
||||
.bs-popover-auto[x-placement^="left"] > .arrow::after {
|
||||
border-left-color: darken($grey-3, 2%);
|
||||
}
|
||||
// // Season Card Background Color
|
||||
// .season-item {
|
||||
// .card-body {
|
||||
// padding: 0;
|
||||
// }
|
||||
|
||||
.bs-popover-right > .arrow::after,
|
||||
.bs-popover-auto[x-placement^="right"] > .arrow::after {
|
||||
border-right-color: darken($grey-3, 2%) !important;
|
||||
}
|
||||
// .card-header {
|
||||
// background-color: color.adjust(theme.$gray-200, $lightness: -1.5%);
|
||||
// color: theme.$gray-900;
|
||||
// border-bottom: 1px solid theme.$gray-000 !important;
|
||||
// font-family: "Montserrat", sans-serif;
|
||||
// }
|
||||
// }
|
||||
|
||||
.season-item .list-group-item {
|
||||
background-color: $grey-3;
|
||||
// .episode-item {
|
||||
// border-color: theme.$gray-200;
|
||||
// background-color: color.adjust(theme.$gray-300, $lightness: -2%);
|
||||
// color: theme.$gray-800 !important;
|
||||
// border-left-width: 0;
|
||||
// border-right-width: 0;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
// &:hover,
|
||||
// &:active,
|
||||
// &:focus {
|
||||
// background-color: color.adjust(theme.$gray-100, $lightness: -0.75%);
|
||||
// }
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($grey-3, 2.5%);
|
||||
}
|
||||
}
|
||||
// .no-link {
|
||||
// color: inherit;
|
||||
// text-decoration: none;
|
||||
|
||||
// Dark theme popover body
|
||||
.popover-body {
|
||||
color: $grey-8 !important;
|
||||
background-color: darken($grey-3, 2%) !important;
|
||||
}
|
||||
// &:hover {
|
||||
// color: inherit;
|
||||
// text-decoration: none;
|
||||
// }
|
||||
// }
|
||||
|
||||
.season-title {
|
||||
color: $grey-8;
|
||||
cursor: pointer;
|
||||
}
|
||||
// .btn {
|
||||
// box-shadow: none;
|
||||
|
||||
// Season Card Background Color
|
||||
.season-item {
|
||||
.card-body {
|
||||
padding: 0;
|
||||
}
|
||||
// &:focus {
|
||||
// box-shadow: none;
|
||||
// }
|
||||
// }
|
||||
|
||||
.card-header {
|
||||
background-color: darken($grey-2, 1.5%);
|
||||
color: $grey-9;
|
||||
border-bottom: 1px solid $grey-0 !important;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
}
|
||||
// .character-button {
|
||||
// color: theme.$gray-1000;
|
||||
// background-color: theme.$gray-400;
|
||||
// border-color: theme.$gray-300;
|
||||
|
||||
.episode-item {
|
||||
border-color: $grey-2;
|
||||
background-color: darken($grey-3, 2%);
|
||||
color: $grey-8 !important;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
// .badge {
|
||||
// color: color.adjust(theme.$gray-100, $lightness: 8%);
|
||||
// }
|
||||
// }
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: darken($grey-1, 0.75%);
|
||||
}
|
||||
}
|
||||
// .character-button {
|
||||
// &:focus {
|
||||
// background-color: theme.$gray-600 !important;
|
||||
// border-color: theme.$gray-400 !important;
|
||||
|
||||
.no-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
// &:active {
|
||||
// box-shadow: none !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
// &:hover {
|
||||
// background-color: theme.$gray-400 !important;
|
||||
// border-color: theme.$gray-300 !important;
|
||||
// }
|
||||
|
||||
.btn {
|
||||
box-shadow: none;
|
||||
// &:active {
|
||||
// background-color: theme.$gray-300 !important;
|
||||
// border-color: theme.$gray-300 !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
// .character-button > .badge {
|
||||
// background-color: theme.$gray-700;
|
||||
// }
|
||||
|
||||
.character-button {
|
||||
color: $grey-10;
|
||||
background-color: $grey-4;
|
||||
border-color: $grey-3;
|
||||
// /*.btn-dark {*/
|
||||
// /* &:not(:disabled), &:not {*/
|
||||
// /* */
|
||||
// /* }*/
|
||||
// /*}*/
|
||||
|
||||
.badge {
|
||||
color: lighten($grey-11, 8%);
|
||||
}
|
||||
}
|
||||
// /*.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, {*/
|
||||
// /* color: #ffffff;*/
|
||||
// /* background-color: #1d2124;*/
|
||||
// /* border-color: #171a1d;*/
|
||||
// /*}*/
|
||||
|
||||
.character-button {
|
||||
&:focus {
|
||||
background-color: $grey-6 !important;
|
||||
border-color: $grey-4 !important;
|
||||
// .card-footer {
|
||||
// padding: 0.1em;
|
||||
// font-size: 0.8em;
|
||||
// color: grey;
|
||||
// }
|
||||
|
||||
&:active {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
// mark,
|
||||
// .mark {
|
||||
// padding: 0.02em;
|
||||
// background-color: theme.$highlight;
|
||||
// /*color: #black;*/
|
||||
// /*-webkit-filter: invert(100%);*/
|
||||
// /*filter: invert(100%);*/
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-4 !important;
|
||||
border-color: $grey-3 !important;
|
||||
}
|
||||
// //.ais-Hits-item,
|
||||
// //.ais-InfiniteHits-item,
|
||||
// //.ais-InfiniteResults-item,
|
||||
// //.ais-Results-item {
|
||||
// // /*margin-top: 1rem;*/
|
||||
// // /*margin-left: 1rem;*/
|
||||
// // /*padding: 1rem;*/
|
||||
// // /*width: calc(25% - 1rem);*/
|
||||
// // border: none;
|
||||
// // box-shadow: none;
|
||||
// //}
|
||||
|
||||
&:active {
|
||||
background-color: $grey-3 !important;
|
||||
border-color: $grey-3 !important;
|
||||
}
|
||||
}
|
||||
// .card-body h4,
|
||||
// .breadcrumb-item.active {
|
||||
// text-transform: capitalize;
|
||||
// }
|
||||
|
||||
.character-button > .badge {
|
||||
background-color: $grey-7;
|
||||
}
|
||||
// .skeleton {
|
||||
// min-height: 24px;
|
||||
// }
|
||||
|
||||
/*.btn-dark {*/
|
||||
/* &:not(:disabled), &:not {*/
|
||||
/* */
|
||||
/* }*/
|
||||
/*}*/
|
||||
// a {
|
||||
// color: #1296ff;
|
||||
|
||||
/*.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, {*/
|
||||
/* color: #ffffff;*/
|
||||
/* background-color: #1d2124;*/
|
||||
/* border-color: #171a1d;*/
|
||||
/*}*/
|
||||
// &:hover {
|
||||
// color: #007fe0;
|
||||
// }
|
||||
// }
|
||||
|
||||
.card-footer {
|
||||
padding: 0.1em;
|
||||
font-size: 0.8em;
|
||||
color: grey;
|
||||
}
|
||||
// .breadcrumb-item + .breadcrumb-item::before {
|
||||
// color: theme.$gray-1000;
|
||||
// }
|
||||
|
||||
mark,
|
||||
.mark {
|
||||
padding: 0.02em;
|
||||
background-color: $highlight;
|
||||
/*color: #black;*/
|
||||
/*-webkit-filter: invert(100%);*/
|
||||
/*filter: invert(100%);*/
|
||||
}
|
||||
// .breadcrumb {
|
||||
// background-color: theme.$gray-300;
|
||||
// border-radius: 0;
|
||||
|
||||
//.ais-Hits-item,
|
||||
//.ais-InfiniteHits-item,
|
||||
//.ais-InfiniteResults-item,
|
||||
//.ais-Results-item {
|
||||
// /*margin-top: 1rem;*/
|
||||
// /*margin-left: 1rem;*/
|
||||
// /*padding: 1rem;*/
|
||||
// /*width: calc(25% - 1rem);*/
|
||||
// border: none;
|
||||
// box-shadow: none;
|
||||
//}
|
||||
// .breadcrumb-item {
|
||||
// color: theme.$gray-1000;
|
||||
// }
|
||||
// }
|
||||
|
||||
.card-body h4,
|
||||
.breadcrumb-item.active {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
// .list-group {
|
||||
// .list-group-item {
|
||||
// background-color: theme.$gray-200;
|
||||
// }
|
||||
// }
|
||||
|
||||
.skeleton {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1296ff;
|
||||
|
||||
&:hover {
|
||||
color: #007fe0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: $grey-10;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
background-color: $grey-3;
|
||||
border-radius: 0;
|
||||
|
||||
.breadcrumb-item {
|
||||
color: $grey-10;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group {
|
||||
.list-group-item {
|
||||
background-color: $grey-2;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top-color: $grey-6;
|
||||
}
|
||||
// hr {
|
||||
// border-top-color: theme.$gray-600;
|
||||
// }
|
||||
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import axios from "axios";
|
||||
import {types} from "@/mutation_types";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
// Generate 'base' representing episode data
|
||||
const episodeCount = [6, 22, 23, 14, 26, 24, 24, 24, 23];
|
||||
const baseData = Array.from({length: 9}, (x, season) => {
|
||||
// Array of null values representing each episode
|
||||
const episodeData = Array.from({length: episodeCount[season]}, (x, episode) => {
|
||||
return {episode_id: episode + 1, loaded: false}
|
||||
})
|
||||
return {season_id: season + 1, episodes: episodeData};
|
||||
})
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
seasonCount: 9,
|
||||
episodeCount: episodeCount,
|
||||
quoteData: baseData,
|
||||
preloaded: {episodes: false, characters: false},
|
||||
characters_loaded: false,
|
||||
characters: {}
|
||||
},
|
||||
mutations: {
|
||||
// Fully set episode data
|
||||
[types.SET_EPISODE](state, payload) {
|
||||
state.quoteData[payload.season - 1].episodes[payload.episode - 1] = payload.episodeData
|
||||
},
|
||||
// Merge many episodes data simultaneously
|
||||
[types.MERGE_EPISODES](state, payload) {
|
||||
for (const season of payload) {
|
||||
for (const episode of season) {
|
||||
if (episode === null) {
|
||||
console.log(`Missing Episode`)
|
||||
continue;
|
||||
}
|
||||
|
||||
const s = episode.seasonNumber - 1;
|
||||
const e = episode.episodeNumber - 1;
|
||||
state.quoteData[s].episodes[e] = Object.assign(state.quoteData[s].episodes[e], episode);
|
||||
|
||||
// If scenes are included for some reason, mark as a fully loaded episode
|
||||
if (episode.scenes !== undefined)
|
||||
state.quoteData[s].episodes[e].loaded = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 'Merge' episode data, overwriting existing attributes as needed
|
||||
[types.MERGE_EPISODE](state, payload) {
|
||||
const s = payload.season - 1;
|
||||
const e = payload.episode - 1;
|
||||
state.quoteData[s].episodes[e] = Object.assign(state.quoteData[s].episodes[e], payload.episodeData);
|
||||
|
||||
// If the episodeData has scenes, it means that this is a full episode data merge - mark it as 'loaded'
|
||||
if (payload.episodeData.scenes !== undefined)
|
||||
state.quoteData[s].episodes[e].loaded = true;
|
||||
},
|
||||
[types.SET_PRELOADED](state, payload) {
|
||||
state.preloaded[payload.type] = payload.status;
|
||||
},
|
||||
[types.SET_CHARACTER](state, payload) {
|
||||
state.characters[payload.id] = payload.characterData
|
||||
},
|
||||
[types.MERGE_CHARACTERS](state, payload) {
|
||||
// Iterate and store.
|
||||
for (const [charId, charData] of Object.entries(payload.characters)) {
|
||||
Vue.set(state.characters, charId, charData)
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// Perform async API call to fetch specific Episode data
|
||||
[types.FETCH_EPISODE](context, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Don't re-fetch API data if it's already loaded
|
||||
if (context.getters.isFetched(payload.season, payload.episode)) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const path = `/json/${payload.season.toString().padStart(2, "0")}/${payload.episode.toString().padStart(2, "0")}.json`;
|
||||
axios.get(path)
|
||||
.then((res) => {
|
||||
// Push episode data
|
||||
context.commit(types.MERGE_EPISODE, {
|
||||
season: payload.season,
|
||||
episode: payload.episode,
|
||||
episodeData: res.data
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
reject()
|
||||
});
|
||||
})
|
||||
},
|
||||
[types.PRELOAD_EPISODES]({commit}) {
|
||||
const path = `/json/episodes.json`;
|
||||
|
||||
axios.get(path)
|
||||
.then((res) => {
|
||||
commit(types.MERGE_EPISODES, res.data)
|
||||
commit(types.SET_PRELOADED, {type: 'episodes', status: true});
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
})
|
||||
},
|
||||
async [types.PRELOAD_CHARACTERS]({commit, getters}) {
|
||||
if (getters.checkPreloaded('characters'))
|
||||
return
|
||||
|
||||
const path = `/json/characters.json`;
|
||||
let res = null;
|
||||
try {
|
||||
res = await axios.get(path)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error
|
||||
}
|
||||
|
||||
commit(types.MERGE_CHARACTERS, {characters: res.data})
|
||||
commit(types.SET_PRELOADED, {type: 'characters', status: true})
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
checkPreloaded: (state) => (identifier) => {
|
||||
// Check whether a certain resource identifier is preloaded
|
||||
return state.preloaded[identifier] === true;
|
||||
},
|
||||
// Check whether a episode has been fetched yet
|
||||
isFetched: (state) => (season, episode) => {
|
||||
const ep = state.quoteData[season - 1].episodes[episode - 1];
|
||||
return ep.loaded;
|
||||
},
|
||||
// Get the number of episodes present for a given season
|
||||
getEpisodeCount: (state) => (season) => {
|
||||
return state.episodeCount[season - 1];
|
||||
},
|
||||
// return Episode data if present
|
||||
getEpisode: (state, getters) => (season, episode) => {
|
||||
if (getters.isFetched(season, episode)) {
|
||||
return state.quoteData[season - 1].episodes[episode - 1];
|
||||
} else
|
||||
return null
|
||||
},
|
||||
// return true if a specific episode is valid
|
||||
isValidEpisode: (state, getters) => (season, episode = 1) => {
|
||||
return season >= 1 && season <= 9 && episode >= 1 && episode <= getters.getEpisodeCount(season)
|
||||
},
|
||||
getCharacter: (state) => (character_id) => {
|
||||
return state.characters[character_id];
|
||||
},
|
||||
getSortedCharacters: (state) => () => {
|
||||
let keys = Object.keys(state.characters);
|
||||
console.log(keys)
|
||||
keys.sort((a, b) => {
|
||||
const a_count = state.characters[a].appearances;
|
||||
const b_count = state.characters[b].appearances
|
||||
if (a_count < b_count) return 1;
|
||||
else return -1;
|
||||
})
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
});
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
|
||||
export interface Character {
|
||||
name: string;
|
||||
appearances: number;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface Season {
|
||||
season_id: number;
|
||||
episodes: Episode[];
|
||||
}
|
||||
|
||||
export type LoadedEpisode = {
|
||||
loaded: true;
|
||||
title: string;
|
||||
description: string;
|
||||
scenes: [Scene, ...Scene[]];
|
||||
characters: Record<string, Character>;
|
||||
};
|
||||
|
||||
export type PreloadedEpisode = {
|
||||
loaded: false;
|
||||
title: string;
|
||||
description: string;
|
||||
scenes: [];
|
||||
characters: Record<string, Character>;
|
||||
};
|
||||
|
||||
export type UnloadedEpisode = {
|
||||
loaded: false;
|
||||
scenes: [];
|
||||
};
|
||||
|
||||
export type Episode = {
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
} & (LoadedEpisode | PreloadedEpisode | UnloadedEpisode);
|
||||
|
||||
export interface Scene {
|
||||
scene_id: number;
|
||||
scene_number: number;
|
||||
scene_name: string;
|
||||
}
|
||||
|
||||
export interface Preloaded {
|
||||
episodes: boolean;
|
||||
characters: boolean;
|
||||
}
|
||||
|
||||
// Generate 'base' representing episode data
|
||||
const episodeCount = [6, 22, 23, 14, 26, 24, 24, 24, 23];
|
||||
const baseData: Season[] = Array.from({ length: 9 }, (_, season) => {
|
||||
// Array of null values representing each episode
|
||||
const episodeData: Episode[] = Array.from({ length: episodeCount[season] }, (_, episode) => {
|
||||
return { seasonNumber: season + 1, episodeNumber: episode + 1, loaded: false, scenes: [] };
|
||||
});
|
||||
return { season_id: season + 1, episodes: episodeData };
|
||||
});
|
||||
|
||||
const useStore = defineStore('main', {
|
||||
state: () => {
|
||||
return {
|
||||
seasonCount: 9 as number,
|
||||
episodeCount: episodeCount as number[],
|
||||
quoteData: baseData as Season[],
|
||||
preloaded: { episodes: false, characters: false } as Preloaded,
|
||||
characters_loaded: false as boolean,
|
||||
characters: {} as Record<string, Character>,
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
// Fully set episode data
|
||||
setEpisode(payload: { season: number; episode: number; episodeData: Episode }) {
|
||||
this.quoteData[payload.season - 1].episodes[payload.episode - 1] = payload.episodeData;
|
||||
},
|
||||
// Merge many episodes data simultaneously
|
||||
mergeEpisodes(payload: Episode[][]) {
|
||||
for (const season of payload) {
|
||||
for (const episode of season) {
|
||||
if (episode === null) {
|
||||
console.log(`Missing Episode`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const s = episode.seasonNumber! - 1;
|
||||
const e = episode.episodeNumber! - 1;
|
||||
this.quoteData[s].episodes[e] = Object.assign(this.quoteData[s].episodes[e], episode);
|
||||
|
||||
// If scenes are included for some reason, mark as a fully loaded episode
|
||||
if (episode.scenes !== undefined) this.quoteData[s].episodes[e].loaded = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 'Merge' episode data, overwriting existing attributes as needed
|
||||
mergeEpisode(payload: { season: number; episode: number; episodeData: Episode }) {
|
||||
const s = payload.season - 1;
|
||||
const e = payload.episode - 1;
|
||||
this.quoteData[s].episodes[e] = Object.assign(
|
||||
this.quoteData[s].episodes[e],
|
||||
payload.episodeData,
|
||||
);
|
||||
|
||||
// If the episodeData has scenes, it means that this is a full episode data merge - mark it as 'loaded'
|
||||
if (payload.episodeData.scenes !== undefined) this.quoteData[s].episodes[e].loaded = true;
|
||||
},
|
||||
setPreloaded(payload: { type: keyof Preloaded; status: boolean }) {
|
||||
this.preloaded[payload.type] = payload.status;
|
||||
},
|
||||
setCharacter(payload: { id: string; characterData: Character }) {
|
||||
this.characters[payload.id] = payload.characterData;
|
||||
},
|
||||
mergeCharacters(payload: { characters: Record<string, Character> }) {
|
||||
// Iterate and store.
|
||||
for (const [charId, charData] of Object.entries(payload.characters)) {
|
||||
this.characters[charId] = charData;
|
||||
}
|
||||
},
|
||||
// Perform async API call to fetch specific Episode data
|
||||
fetchEpisode(payload: { season: number; episode: number }): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Don't re-fetch API data if it's already loaded
|
||||
if (this.isFetched(payload.season, payload.episode)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const path = `/json/${payload.season.toString().padStart(2, '0')}/${payload.episode.toString().padStart(2, '0')}.json`;
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
// Push episode data
|
||||
this.mergeEpisode({
|
||||
season: payload.season,
|
||||
episode: payload.episode,
|
||||
episodeData: res.data,
|
||||
});
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
preloadEpisodes(): void {
|
||||
const path = `/json/episodes.json`;
|
||||
|
||||
axios
|
||||
.get(path)
|
||||
.then((res) => {
|
||||
this.mergeEpisodes(res.data as Episode[][]);
|
||||
this.setPreloaded({ type: 'episodes', status: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
async preloadCharacters(): Promise<void> {
|
||||
if (this.checkPreloaded('characters')) return;
|
||||
|
||||
const path = `/json/characters.json`;
|
||||
let res = null;
|
||||
try {
|
||||
res = await axios.get(path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.mergeCharacters({ characters: res.data });
|
||||
this.setPreloaded({ type: 'characters', status: true });
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
checkPreloaded:
|
||||
(state) =>
|
||||
(identifier: keyof Preloaded): boolean => {
|
||||
// Check whether a certain resource identifier is preloaded
|
||||
return state.preloaded[identifier] === true;
|
||||
},
|
||||
// Check whether a episode has been fetched yet
|
||||
isFetched:
|
||||
(state) =>
|
||||
(season: number, episode: number): boolean => {
|
||||
const ep = state.quoteData[season - 1]?.episodes[episode - 1];
|
||||
return ep.loaded;
|
||||
},
|
||||
// Get the number of episodes present for a given season
|
||||
getEpisodeCount:
|
||||
(state) =>
|
||||
(season: number): number => {
|
||||
return state.episodeCount[season - 1];
|
||||
},
|
||||
// return Episode data if present
|
||||
getEpisode:
|
||||
(state) =>
|
||||
(season: number, episode: number): Episode | null => {
|
||||
return state.quoteData[season - 1]?.episodes[episode - 1] ?? null;
|
||||
},
|
||||
// return true if a specific episode is valid
|
||||
isValidEpisode:
|
||||
(state) =>
|
||||
(season: number, episode: number = 1): boolean => {
|
||||
return (
|
||||
season >= 1 && season <= 9 && episode >= 1 && episode <= state.episodeCount[season - 1]
|
||||
);
|
||||
},
|
||||
getCharacter:
|
||||
(state) =>
|
||||
(character_id: string): Character | undefined => {
|
||||
return state.characters[character_id];
|
||||
},
|
||||
getSortedCharacters: (state) => (): string[] => {
|
||||
const keys = Object.keys(state.characters);
|
||||
console.log(keys);
|
||||
keys.sort((a, b) => {
|
||||
const a_count = state.characters[a].appearances;
|
||||
const b_count = state.characters[b].appearances;
|
||||
if (a_count < b_count) return 1;
|
||||
else return -1;
|
||||
});
|
||||
return keys;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default useStore;
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"include": ["src/**/__tests__/*", "env.d.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
|
||||
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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'),
|
||||
'@assets': path.resolve(__dirname, './public'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: './build',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
const {gitDescribeSync} = require('git-describe');
|
||||
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash
|
||||
|
||||
module.exports = {
|
||||
outputDir: "./build",
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
prependData: '@import "@/scss/_variables.scss";'
|
||||
}
|
||||
}
|
||||
},
|
||||
chainWebpack: config => {
|
||||
config.module
|
||||
.rule('vue')
|
||||
.use('vue-loader')
|
||||
.loader('vue-loader')
|
||||
.tap(options => {
|
||||
options.transformAssetUrls = {
|
||||
img: 'src',
|
||||
image: 'xlink:href',
|
||||
'b-avatar': 'src',
|
||||
'b-img': 'src',
|
||||
'b-img-lazy': ['src', 'blank-src'],
|
||||
'b-card': 'img-src',
|
||||
'b-card-img': 'src',
|
||||
'b-card-img-lazy': ['src', 'blank-src'],
|
||||
'b-carousel-slide': 'img-src',
|
||||
'b-embed': 'src'
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user