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:
2025-07-15 18:34:18 -05:00
parent 57c9451f3f
commit ccd975d181
59 changed files with 7989 additions and 11441 deletions
+9
View File
@@ -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
-30
View File
@@ -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}
}]
}
}
+2
View File
@@ -1,3 +1,5 @@
* text=auto eol=lf
data/normalization/html/* linguist-vendored
public/json/** linguist-generated
public/img/** linguist-generated
+3 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"plugins": ["prettier-plugin-tailwindcss"],
"singleQuote": true,
"printWidth": 100
}
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}
+13
View File
@@ -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"
}
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};
+20
View File
@@ -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"
}
Vendored
+7
View File
@@ -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
}
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+5568
View File
File diff suppressed because it is too large Load Diff
+111 -160
View File
@@ -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>
Binary file not shown.
+57
View File
@@ -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
View File
@@ -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>
+91 -71
View File
@@ -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>
+34 -23
View File
@@ -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
View File
@@ -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>
+41 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+9 -5
View File
@@ -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;
}
+74 -59
View File
@@ -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
View File
@@ -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}&quote=${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}&quote=${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>
+14 -9
View File
@@ -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
View File
@@ -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>
+58 -68
View File
@@ -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>
+53 -56
View File
@@ -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">
&OpenCurlyDoubleQuote;{{ title }}&CloseCurlyDoubleQuote;
</span>
</RouterLink>
</template>
</style>
<style scoped></style>
+93 -83
View File
@@ -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>
+19
View File
@@ -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>
+4
View File
@@ -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
View File
@@ -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');
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
-34
View File
@@ -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
View File
@@ -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');
-11
View File
@@ -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',
}
-68
View File
@@ -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,
};
},
});
+66
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+13
View File
@@ -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/*"]
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+19
View File
@@ -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"]
}
}
+11
View File
@@ -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"]
}
}
+19
View File
@@ -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',
},
});
+14
View File
@@ -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)),
},
}),
)
-35
View File
@@ -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
})
}
};
-9515
View File
File diff suppressed because it is too large Load Diff