Initial commit

This commit is contained in:
Moritz Ruth 2024-05-19 20:29:37 +02:00
commit 564cc84db1
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
14 changed files with 2864 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/node_modules/
/dist/
/.idea/
*.env

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20.11

14
index.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tea Dashboard (Loading…)</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module" src="./src/main.ts"></script>
<link rel="stylesheet" href="./src/global.css"/>
</head>
<body>
<div id="app"></div>
<div id="tooltips"></div>
</body>
</html>

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@tea-project/web-frontend",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.12",
"@vitejs/plugin-vue": "^5.0.4",
"browserslist": "^4.23.0",
"modern-normalize": "^2.0.0",
"sass": "^1.77.2",
"type-fest": "^4.18.2",
"typescript": "^5.4.5",
"unocss": "^0.60.2",
"unplugin-icons": "^0.19.0",
"unplugin-vue-router": "^0.8.6",
"vite": "^5.2.11",
"windicss": "^3.5.6"
},
"dependencies": {
"@fontsource-variable/fraunces": "^5.0.21",
"@fontsource-variable/manrope": "^5.0.20",
"@iconify-json/solar": "^1.1.9",
"@iconify-json/svg-spinners": "^1.1.2",
"@unhead/vue": "^1.9.10",
"@vueuse/core": "^10.9.0",
"lodash-es": "^4.17.21",
"vue": "^3.4.27",
"vue-router": "^4.3.2"
}
}

2494
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

21
src/App.vue Normal file
View file

@ -0,0 +1,21 @@
<template>
<NavigationMenuWrapper>
<div class="p-8 flex-shrink-0 flex-grow-0">
<SuspenseRouterView/>
</div>
</NavigationMenuWrapper>
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
import { useHead } from "@unhead/vue"
import SuspenseRouterView from "./components/reusable/SuspenseRouterView.vue"
import NavigationMenuWrapper from "@/components/NavigationMenuWrapper.vue"
useHead({
titleTemplate: title => title === undefined ? "Tea Dashboard" : `${title} — Tea Dashboard`
})
</script>

View file

@ -0,0 +1,71 @@
<template>
<nav class="w-70 bg-gray-900 fixed top-0 left-0 bottom-0 p-8 flex flex-col gap-3" :class="$style.root">
<ul class="list-none p-0 grid grid-cols-2 gap-1">
<li v-for="type in mediaTypes" :key="type.id">
<router-link
class="relative rounded-sm w-full p-2 no-underline flex flex-col items-center gap-1 group"
:to="`/media/${type.id}`"
:style="{ color: type.color }"
>
<component class="absolute -top-5 left-0 opacity-5 blur-5 pointer-events-none group-hover:blur-lg group-hover:opacity-30 transition duration-300" :is="type.blob"/>
<component class="text-3xl" :is="type.icon"/>
<span class="text-base">{{ type.label }}</span>
</router-link>
</li>
</ul>
</nav>
<div class="pl-70">
<slot/>
</div>
</template>
<style module lang="scss">
.root {
box-shadow: 0 0 4px 0 theme("colors.gray.700");
&::before {
content: "";
position: absolute;
pointer-events: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url("../assets/noise.png") repeat;
opacity: 15%;
}
}
</style>
<script setup lang="ts">
import type { Component } from "vue"
import BooksIcon from "virtual:icons/ph/books-light"
import FilmStripIcon from "virtual:icons/ph/film-strip-light"
import LiteratureBlob from "./blobs/LiteratureBlob.vue"
import MoviesBlob from "./blobs/MoviesBlob.vue"
interface MediaType {
id: string
label: string
icon: Component
blob: Component
color: string
}
const mediaTypes: MediaType[] = [
{
id: "literature",
label: "Literature",
icon: BooksIcon,
blob: LiteratureBlob,
color: "#11b970"
},
{
id: "films",
label: "Movies",
icon: FilmStripIcon,
blob: MoviesBlob,
color: "#d2b214"
}
]
</script>

View file

@ -0,0 +1,62 @@
<template>
<router-view v-slot="{ Component }">
<template v-if="Component">
<suspense @pending="loading = true" @resolve="loading = false">
<!-- The key makes that components are not reused if only params changed -->
<component :is="Component" :key="JSON.stringify($route.params)"/>
</suspense>
</template>
</router-view>
<div
class="pt-10 absolute inset-0 bg-background"
:class="$style.loadingOverlay"
:data-is-visible="loading || forceLoading"
>
<div class="flex justify-center gap-3 text-2xl p-8" :class="$style.loadingIndicator">
<LoadingIcon/>
<span>{{ loadingText }}</span>
</div>
</div>
</template>
<style module lang="scss">
.loadingOverlay {
opacity: 0;
transition: 100ms ease opacity;
&[data-is-visible="true"] {
opacity: 100%;
transition-duration: 200ms;
transition-delay: 200ms;
.loadingIndicator {
opacity: 100%;
transition-delay: 1500ms;
}
}
&[data-is-visible="false"] {
pointer-events: none;
}
}
.loadingIndicator {
opacity: 0;
transition: 200ms ease opacity;
}
</style>
<script setup lang="ts">
import { ref } from "vue"
import LoadingIcon from "./LoadingIcon.vue"
defineProps({
loadingText: {
type: String,
default: "Loading…"
},
forceLoading: Boolean
})
const loading = ref(false)
</script>

View file

35
src/global.css Normal file
View file

@ -0,0 +1,35 @@
@import "modern-normalize/modern-normalize.css";
/* fraunces-latin-wght-italic */
@font-face {
font-family: "Fraunces Variable";
font-style: italic;
font-display: block;
font-weight: 100 900;
src: url("@fontsource-variable/fraunces/files/fraunces-latin-wght-italic.woff2") format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* manrope-latin-wght-normal */
@font-face {
font-family: 'Manrope Variable';
font-style: normal;
font-display: swap;
font-weight: 200 800;
src: url(@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
html, body, #app {
@apply font-sans;
width: 100%;
background: theme("colors.gray.3");
color: hsl(0 0% 10%);
min-height: 100vh;
line-height: 1.5;
}
a {
text-decoration: none;
color: currentColor;
}

19
src/main.ts Normal file
View file

@ -0,0 +1,19 @@
import "uno.css"
import { createApp } from "vue"
import App from "./App.vue"
import { createRouter, createWebHistory } from "vue-router/auto"
import { createHead } from "@unhead/vue"
const app = createApp(App)
const router = createRouter({
history: createWebHistory()
})
const head = createHead()
app
.use(router)
.use(head)
.mount("#app")

38
tsconfig.json Normal file
View file

@ -0,0 +1,38 @@
{
"compilerOptions": {
"declaration": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": [
"esnext",
"dom"
],
"module": "esnext",
"moduleResolution": "Bundler",
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": true,
"rootDir": "src",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"stripInternal": true,
"noUncheckedIndexedAccess": false,
"target": "esnext",
"types": [
"vite/client",
"vite-plugin-pages/client",
"unplugin-icons/types/vue",
"unplugin-vue-router/client"
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.vue",
]
}

46
uno.config.ts Normal file
View file

@ -0,0 +1,46 @@
import { defineConfig, transformerDirectives, presetWind } from "unocss"
import colors from "windicss/colors"
const headlessUiVariantRegex = /^ui-(\w+):/
export default defineConfig({
presets: [
presetWind({
dark: "media",
arbitraryVariants: false,
preflight: true
})
],
theme: {
fontFamily: {
sans: `"Manrope Variable", sans-serif`,
serif: "Fraunces Variable, serif",
system: "sans-serif"
},
colors: {
dark: colors.dark,
light: colors.light,
gray: colors.warmGray
}
},
variants: [
matcher => {
const matches = matcher.match(headlessUiVariantRegex)
if (matches === null) {
return matcher
}
const prefix = matches[0]
const state = matches[1]
return {
matcher: matcher.slice(prefix.length),
selector: s => `${s}[data-headlessui-state~="${state}"]`
}
}
],
transformers: [
transformerDirectives()
]
})

23
vite.config.ts Normal file
View file

@ -0,0 +1,23 @@
import { defineConfig } from "vite"
import vuePlugin from "@vitejs/plugin-vue"
import iconsPlugin from "unplugin-icons/vite"
import unoCssPlugin from "unocss/vite"
import vueRouterPlugin from "unplugin-vue-router/vite"
import { resolve } from "node:path"
export default defineConfig({
plugins: [
vueRouterPlugin({
importMode: "async",
dts: "./src/generated-types/vue-router.d.ts"
}),
vuePlugin(),
iconsPlugin(),
unoCssPlugin()
],
resolve: {
alias: {
"@": resolve(__dirname, "./src")
}
}
})