Initial commit
This commit is contained in:
commit
564cc84db1
14 changed files with 2864 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
/.idea/
|
||||
*.env
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
20.11
|
14
index.html
Normal file
14
index.html
Normal 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
36
package.json
Normal 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
2494
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
21
src/App.vue
Normal file
21
src/App.vue
Normal 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>
|
71
src/components/NavigationMenuWrapper.vue
Normal file
71
src/components/NavigationMenuWrapper.vue
Normal 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>
|
62
src/components/SuspenseRouterView.vue
Normal file
62
src/components/SuspenseRouterView.vue
Normal 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>
|
0
src/generated-types/.gitkeep
Normal file
0
src/generated-types/.gitkeep
Normal file
35
src/global.css
Normal file
35
src/global.css
Normal 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
19
src/main.ts
Normal 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
38
tsconfig.json
Normal 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
46
uno.config.ts
Normal 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
23
vite.config.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Add table
Reference in a new issue