From fca7b288322dbde905221cb373a0ce3ae6d3b513 Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 03:17:13 +0700 Subject: [PATCH 01/31] docs: add web-vue mirror context and glossary --- CONTEXT-MAP.md | 1 + apps/web-vue/CONTEXT.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 apps/web-vue/CONTEXT.md diff --git a/CONTEXT-MAP.md b/CONTEXT-MAP.md index 0f6cbb8..4b99b87 100644 --- a/CONTEXT-MAP.md +++ b/CONTEXT-MAP.md @@ -6,5 +6,6 @@ This repo has four contexts, one per app. Each context has its own `CONTEXT.md` - **api-go** — Go port of the API. See `apps/api-go/CONTEXT.md`. - **api-axum** — Rust Axum port of the API. See `apps/api-axum/CONTEXT.md`. - **web** — frontend web app. See `apps/web/CONTEXT.md` (created lazily by `/domain-modeling`). +- **web-vue** — Vue mirror of web. See `apps/web-vue/CONTEXT.md`. System-wide decisions live in `docs/adr/`. diff --git a/apps/web-vue/CONTEXT.md b/apps/web-vue/CONTEXT.md new file mode 100644 index 0000000..e28796c --- /dev/null +++ b/apps/web-vue/CONTEXT.md @@ -0,0 +1,11 @@ +# CONTEXT — web-vue + +## Language + +**Mirror**: +A separate app that reproduces another app's pages and behavior against the same API, kept running alongside the original. The original stays authoritative until parity is proven. +_Avoid_: clone, fork, port + +**Mirror page**: +One route in the mirror that answers to the same route in `web`, with the same visible structure and behavior. Visual parity may be approximate where no equivalent component library exists (calendar). +_Avoid_: copy, twin From c11b5c7093aa7d99658027bd90a7b83103354237 Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 03:48:31 +0700 Subject: [PATCH 02/31] feat(web-vue): scaffold Vite+Vue3 SPA with Tailwind v4, router, vue-query, unhead Part of #40 --- apps/web-vue/.env.example | 3 + apps/web-vue/index.html | 23 ++ apps/web-vue/package.json | 54 +++ apps/web-vue/src/App.vue | 5 + apps/web-vue/src/assets/globals.css | 427 ++++++++++++++++++++ apps/web-vue/src/lib/cn.ts | 6 + apps/web-vue/src/lib/constants.ts | 3 + apps/web-vue/src/main.ts | 14 + apps/web-vue/src/pages/Home.vue | 5 + apps/web-vue/src/router/index.ts | 8 + apps/web-vue/tsconfig.json | 21 + apps/web-vue/vite.config.ts | 14 + bun.lock | 605 ++++++++++++++++++++++++++-- 13 files changed, 1162 insertions(+), 26 deletions(-) create mode 100644 apps/web-vue/.env.example create mode 100644 apps/web-vue/index.html create mode 100644 apps/web-vue/package.json create mode 100644 apps/web-vue/src/App.vue create mode 100644 apps/web-vue/src/assets/globals.css create mode 100644 apps/web-vue/src/lib/cn.ts create mode 100644 apps/web-vue/src/lib/constants.ts create mode 100644 apps/web-vue/src/main.ts create mode 100644 apps/web-vue/src/pages/Home.vue create mode 100644 apps/web-vue/src/router/index.ts create mode 100644 apps/web-vue/tsconfig.json create mode 100644 apps/web-vue/vite.config.ts diff --git a/apps/web-vue/.env.example b/apps/web-vue/.env.example new file mode 100644 index 0000000..9a36741 --- /dev/null +++ b/apps/web-vue/.env.example @@ -0,0 +1,3 @@ +VITE_API_URL=http://localhost:4000/v1 +VITE_SITE_URL=http://localhost:3001 +VITE_CDN_URL=https://cdn.wds.xirothedev.site diff --git a/apps/web-vue/index.html b/apps/web-vue/index.html new file mode 100644 index 0000000..ac816e7 --- /dev/null +++ b/apps/web-vue/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + + diff --git a/apps/web-vue/package.json b/apps/web-vue/package.json new file mode 100644 index 0000000..0ac4d65 --- /dev/null +++ b/apps/web-vue/package.json @@ -0,0 +1,54 @@ +{ + "name": "web-vue", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@fullcalendar/core": "^6.1.21", + "@fullcalendar/daygrid": "^6.1.21", + "@fullcalendar/interaction": "^6.1.21", + "@fullcalendar/timegrid": "^6.1.21", + "@fullcalendar/vue3": "^6.1.21", + "@tanstack/vue-query": "^5.102.8", + "@tiptap/extension-code-block": "^3.30.6", + "@tiptap/extension-link": "^3.30.6", + "@tiptap/extension-placeholder": "^3.30.6", + "@tiptap/markdown": "^3.30.6", + "@tiptap/pm": "^3.30.6", + "@tiptap/starter-kit": "^3.30.6", + "@tiptap/vue-3": "^3.30.6", + "@unhead/vue": "^3.4.0", + "@vee-validate/zod": "^4.15.1", + "axios": "^1.20.0", + "chart.js": "^4.5.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "countup.js": "^2.10.1", + "lucide-vue-next": "^1.0.0", + "marked": "^18.0.11", + "motion-v": "^2.4.0", + "reka-ui": "^2.10.4", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "unhead": "^3.4.0", + "vee-validate": "^4.15.1", + "vue": "^3.5.42", + "vue-chartjs": "^5.3.4", + "vue-router": "^5.3.0", + "zod": "^4.5.4" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.4.0", + "@vitejs/plugin-vue": "^6.0.8", + "tailwindcss": "^4.3.3", + "typescript": "^5.9.0", + "vite": "^8.2.2", + "vue-tsc": "^3.3.11" + } +} diff --git a/apps/web-vue/src/App.vue b/apps/web-vue/src/App.vue new file mode 100644 index 0000000..613fac0 --- /dev/null +++ b/apps/web-vue/src/App.vue @@ -0,0 +1,5 @@ + + + diff --git a/apps/web-vue/src/assets/globals.css b/apps/web-vue/src/assets/globals.css new file mode 100644 index 0000000..9ce15cf --- /dev/null +++ b/apps/web-vue/src/assets/globals.css @@ -0,0 +1,427 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --card: oklch(1 0 0); + --border: oklch(0.922 0 0); + --accent: oklch(0.97 0 0); + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 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-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --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); + --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); +} + +@theme inline { + /* WebDev Studios Color Palette */ + --color-wds-background: #000000; + --color-wds-text: #ffffff; + --color-wds-accent: #f7931e; + --color-wds-secondary: #fff8e1; + + /* Original colors (kept for compatibility) */ + --color-primary: #08090a; + --color-card: #0f1011; + --color-border: #23252a; + --color-accent: #6366f1; + --font-sans: var(--font-inter); + + /* Animations */ + --animate-shimmer: shimmer 2s linear infinite; + --animate-beam: beam 4s linear infinite; + --animate-blob-1: blob-1 12s ease-in-out infinite; + --animate-blob-2: blob-2 12s ease-in-out infinite; + --animate-blob-3: blob-3 10s ease-in-out infinite; + --animate-blob-4: blob-4 10s ease-in-out infinite; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +body { + background-color: #ffffff; + color: #000000; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + font-family: + var(--font-inter), + system-ui, + -apple-system, + sans-serif; +} + +/* WebDev Studios Color Variables */ +:root { + --wds-black: #000000; + --wds-white: #ffffff; + --wds-orange: #f7931e; + --wds-cream: #fff8e1; +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #f5f5f5; +} + +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +/* Retro Grid Pattern */ +.retro-grid { + background-size: 40px 40px; + background-image: + linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px), + linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + mask-image: radial-gradient(ellipse 60% 50% at 50% 0%, #000 70%, transparent 100%); +} + +/* Utility for 3D Tilt */ +.tilt-card { + transform: perspective(1200px) rotateX(15deg); +} + +/* Glassmorphism utility */ +.glass { + background: rgba(11, 11, 11, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +/* Glassmorphism card for forms */ +.glass-card { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + border-radius: 1rem; + padding: 2rem; +} + +/* Glassmorphism input */ +.glass-input { + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + color: white; + transition: all 0.3s ease; +} + +.glass-input::placeholder { + color: rgba(255, 255, 255, 0.4); +} + +.glass-input:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.25); +} + +.glass-input:focus { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(247, 147, 30, 0.6); + box-shadow: + 0 0 0 2px rgba(247, 147, 30, 0.2), + 0 0 20px rgba(247, 147, 30, 0.1); + outline: none; +} + +.glass-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Glassmorphism gradient button - Simple cursor.com style */ +.glass-button { + position: relative; + background: rgb(247, 147, 30); + border: 1px solid rgba(247, 147, 30, 0.8); + color: white; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.15s ease; +} + +.glass-button:hover:not(:disabled) { + background: rgb(255, 159, 67); + border-color: rgb(255, 159, 67); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.glass-button:active:not(:disabled) { + background: rgb(230, 130, 20); + border-color: rgb(230, 130, 20); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.glass-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Gradient shift animation for button */ +@keyframes gradient-shift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +/* Selection */ +::selection { + background-color: rgba(247, 147, 30, 0.3); + color: rgb(255, 255, 255); +} + +/* Keyframes */ +@keyframes shimmer { + from { + background-position: 0 0; + } + to { + background-position: -200% 0; + } +} + +@keyframes beam { + 0%, + 100% { + opacity: 0; + } + 50% { + opacity: 1; + } +} + +/* Blob animations for glassmorphism background */ +@keyframes blob-1 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(30px, -50px) scale(1.1); + } + 66% { + transform: translate(-20px, 20px) scale(0.9); + } +} + +@keyframes blob-2 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 33% { + transform: translate(-30px, 50px) scale(0.9); + } + 66% { + transform: translate(20px, -20px) scale(1.1); + } +} + +@keyframes blob-3 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(40px, 30px) scale(1.05); + } +} + +@keyframes blob-4 { + 0%, + 100% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(-40px, -30px) scale(1.05); + } +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 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.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 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(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + + /* Remove number input spinner arrows */ + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + input[type='number'] { + -moz-appearance: textfield; + } + + /* Shiki code block styles - preserve inline styles from theme */ + /* Shiki generates inline styles with color attributes, preserve them */ + /* Override background to transparent */ + pre[style] { + /* Override Shiki's background to transparent */ + background: transparent !important; + margin: 0 !important; + padding: 1rem !important; + /* Add border for better visual separation */ + border: 1px solid rgb(247 147 30 / 0.2) !important; + border-radius: 0.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; + } + + pre code { + /* Preserve Shiki's code styles */ + display: block; + width: 100%; + font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace; + /* Counter for line numbers */ + counter-reset: line; + line-height: 0.5; + } + + /* Line numbers styling using CSS counter */ + pre code .line { + display: block; + min-height: 0.375rem; + padding-left: 4rem; + padding-right: 1rem; + padding-top: 0; + padding-bottom: 0; + position: relative; + counter-increment: line; + line-height: 0.5; + } + + /* Line number using ::before pseudo-element */ + pre code .line::before { + content: counter(line); + position: absolute; + left: 0; + width: 3rem; + padding-right: 1rem; + text-align: right; + color: rgb(255 255 255 / 0.3); + user-select: none; + border-right: 1px solid rgb(247 147 30 / 0.1); + padding-left: 0.5rem; + } + + /* Preserve all inline color styles from Shiki */ + /* Do NOT override inline styles - Shiki uses inline styles for colors */ + /* React will automatically apply inline styles from Shiki */ +} diff --git a/apps/web-vue/src/lib/cn.ts b/apps/web-vue/src/lib/cn.ts new file mode 100644 index 0000000..9ad0df4 --- /dev/null +++ b/apps/web-vue/src/lib/cn.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/apps/web-vue/src/lib/constants.ts b/apps/web-vue/src/lib/constants.ts new file mode 100644 index 0000000..68885d5 --- /dev/null +++ b/apps/web-vue/src/lib/constants.ts @@ -0,0 +1,3 @@ +export const API_URL = import.meta.env.VITE_API_URL as string; +export const SITE_URL = import.meta.env.VITE_SITE_URL as string; +export const CDN_URL = import.meta.env.VITE_CDN_URL as string; diff --git a/apps/web-vue/src/main.ts b/apps/web-vue/src/main.ts new file mode 100644 index 0000000..9ab2794 --- /dev/null +++ b/apps/web-vue/src/main.ts @@ -0,0 +1,14 @@ +import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; +import { createHead } from '@unhead/vue/client'; +import { createApp } from 'vue'; + +import App from '@/App.vue'; +import '@/assets/globals.css'; +import { router } from '@/router'; + +const app = createApp(App); + +app.use(createHead()); +app.use(VueQueryPlugin, { queryClient: new QueryClient() }); +app.use(router); +app.mount('#app'); diff --git a/apps/web-vue/src/pages/Home.vue b/apps/web-vue/src/pages/Home.vue new file mode 100644 index 0000000..0203ec6 --- /dev/null +++ b/apps/web-vue/src/pages/Home.vue @@ -0,0 +1,5 @@ + + + diff --git a/apps/web-vue/src/router/index.ts b/apps/web-vue/src/router/index.ts new file mode 100644 index 0000000..fa7eeaa --- /dev/null +++ b/apps/web-vue/src/router/index.ts @@ -0,0 +1,8 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +import Home from '@/pages/Home.vue'; + +export const router = createRouter({ + history: createWebHistory(), + routes: [{ path: '/', component: Home }], +}); diff --git a/apps/web-vue/tsconfig.json b/apps/web-vue/tsconfig.json new file mode 100644 index 0000000..290bc47 --- /dev/null +++ b/apps/web-vue/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "useDefineForClassFields": true, + "jsx": "preserve", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] +} diff --git a/apps/web-vue/vite.config.ts b/apps/web-vue/vite.config.ts new file mode 100644 index 0000000..161b002 --- /dev/null +++ b/apps/web-vue/vite.config.ts @@ -0,0 +1,14 @@ +import tailwindcss from '@tailwindcss/vite'; +import vue from '@vitejs/plugin-vue'; +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { port: 3001 }, +}); diff --git a/bun.lock b/bun.lock index 5221c40..8d77c94 100644 --- a/bun.lock +++ b/bun.lock @@ -183,6 +183,53 @@ "typescript-eslint": "^8.59.4", }, }, + "apps/web-vue": { + "name": "web-vue", + "version": "0.0.0", + "dependencies": { + "@fullcalendar/core": "^6.1.21", + "@fullcalendar/daygrid": "^6.1.21", + "@fullcalendar/interaction": "^6.1.21", + "@fullcalendar/timegrid": "^6.1.21", + "@fullcalendar/vue3": "^6.1.21", + "@tanstack/vue-query": "^5.102.8", + "@tiptap/extension-code-block": "^3.30.6", + "@tiptap/extension-link": "^3.30.6", + "@tiptap/extension-placeholder": "^3.30.6", + "@tiptap/markdown": "^3.30.6", + "@tiptap/pm": "^3.30.6", + "@tiptap/starter-kit": "^3.30.6", + "@tiptap/vue-3": "^3.30.6", + "@unhead/vue": "^3.4.0", + "@vee-validate/zod": "^4.15.1", + "axios": "^1.20.0", + "chart.js": "^4.5.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "countup.js": "^2.10.1", + "lucide-vue-next": "^1.0.0", + "marked": "^18.0.11", + "motion-v": "^2.4.0", + "reka-ui": "^2.10.4", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "unhead": "^3.4.0", + "vee-validate": "^4.15.1", + "vue": "^3.5.42", + "vue-chartjs": "^5.3.4", + "vue-router": "^5.3.0", + "zod": "^4.5.4", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.4.0", + "@vitejs/plugin-vue": "^6.0.8", + "tailwindcss": "^4.3.3", + "typescript": "^5.9.0", + "vite": "^8.2.2", + "vue-tsc": "^3.3.11", + }, + }, }, "trustedDependencies": [ "sharp", @@ -409,6 +456,18 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@floating-ui/vue": ["@floating-ui/vue@1.1.11", "", { "dependencies": { "@floating-ui/dom": "^1.7.6", "@floating-ui/utils": "^0.2.11", "vue-demi": ">=0.13.0" } }, "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw=="], + + "@fullcalendar/core": ["@fullcalendar/core@6.1.21", "", { "dependencies": { "preact": "~10.12.1" } }, "sha512-t3u/+sqh3Iq7TWtUnVLcGDUE6OWZh0UD3c04bI/l7lSLAgAKr3kngBmhHiQD1QXpwC8ZN5iNqG7a7gOVixhSKQ=="], + + "@fullcalendar/daygrid": ["@fullcalendar/daygrid@6.1.21", "", { "peerDependencies": { "@fullcalendar/core": "~6.1.21" } }, "sha512-QYb1y40RGYLlOxKpYWg8O+7njEnKnFG8Tt7qjnubJGR35s1phQg67E+81y2TyAbbm59p2JFOCXGDk9t6KDujIA=="], + + "@fullcalendar/interaction": ["@fullcalendar/interaction@6.1.21", "", { "peerDependencies": { "@fullcalendar/core": "~6.1.21" } }, "sha512-WPYpqtljDWmU0Xm2cOtFrLlocgxv7cgkOppj34Q6OUUat8a6Cnd6kYo2JR+irP223PE5lBYHFNp1qh7SIpJc0w=="], + + "@fullcalendar/timegrid": ["@fullcalendar/timegrid@6.1.21", "", { "dependencies": { "@fullcalendar/daygrid": "~6.1.21" }, "peerDependencies": { "@fullcalendar/core": "~6.1.21" } }, "sha512-2DnShx/jallGmb8QCkr6pAOu/zuPhJrP7+uTrAtSnbqsX7GF3lTxqSeNGkTQwsgF5g/ia8udhQ+JNYaE+TN1cQ=="], + + "@fullcalendar/vue3": ["@fullcalendar/vue3@6.1.21", "", { "peerDependencies": { "@fullcalendar/core": "~6.1.21", "vue": "^3.0.11" } }, "sha512-OGt6WSC+/zz/ej6a0KfIBNl7BYuGchpZU49SsedYyv3WZWbghAE+D8YD6nhH1ia/I4p5Gcsv/nEXgEkT/I8aYQ=="], + "@hono/node-server": ["@hono/node-server@1.19.13", "", { "peerDependencies": { "hono": "4.12.20" } }, "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ=="], "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "0.3.0" }, "peerDependencies": { "react-hook-form": "7.76.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], @@ -505,6 +564,10 @@ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "optionalDependencies": { "@types/node": "25.9.0" } }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + "@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="], + + "@internationalized/number": ["@internationalized/number@3.6.7", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg=="], + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "5.1.2", "string-width-cjs": "npm:string-width@4.2.3", "strip-ansi": "7.2.0", "strip-ansi-cjs": "npm:strip-ansi@6.0.1", "wrap-ansi": "8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -599,6 +662,8 @@ "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], + "@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="], "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ=="], @@ -747,6 +812,38 @@ "@restart/hooks": ["@restart/hooks@0.4.16", "", { "dependencies": { "dequal": "2.0.3" }, "peerDependencies": { "react": "19.2.6" } }, "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], @@ -823,6 +920,10 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + + "@tanstack/match-sorter-utils": ["@tanstack/match-sorter-utils@8.19.4", "", { "dependencies": { "remove-accents": "0.5.0" } }, "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.100.11", "", {}, "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.100.11", "", {}, "sha512-47rVBDuGMW/A4ekt3YQdz+q0JSIwktwGnWCYyQUvSs2/g/Oa+6Fi2/IQk4/Y4vf6u1uwI7hOogHslgMC8f3X/Q=="], @@ -831,6 +932,12 @@ "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.11", "", { "dependencies": { "@tanstack/query-devtools": "5.100.11" }, "peerDependencies": { "@tanstack/react-query": "5.100.11", "react": "19.2.6" } }, "sha512-75RFlJEG53Ed/Cxe5WLmgIpOElPNpgLZq7h0fLFnM5XwTYxSTk1rX/gC6MqGVXsSdrbP7zn7hPSJx9MinwiUHA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="], + + "@tanstack/vue-query": ["@tanstack/vue-query@5.102.8", "", { "dependencies": { "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-core": "5.102.8", "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "peerDependencies": { "@vue/composition-api": "^1.1.2", "vue": "^2.6.0 || ^3.3.0" }, "optionalPeers": ["@vue/composition-api"] }, "sha512-W3Q6Od3FbdttACuavgbICKMFgYCPobvuMiO8tIpqMP073QnREVSaO4ZPqrFqosIbFKikiQTmZhEEbYR9bPsWLA=="], + + "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.36", "", { "dependencies": { "@tanstack/virtual-core": "3.17.8" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-gKpExv4RbB9luVG+SucTXoqPZv/gzu/Yvz6BNO+8kpNxJ2x+I/ulryzl5W9BRciahZGp5Tls3Dp5XP1ztVGbMw=="], + "@tiptap/core": ["@tiptap/core@3.23.4", "", { "peerDependencies": { "@tiptap/pm": "3.23.4" } }, "sha512-ni2LWE52bVeSt3L2HVBSmbBw+elc32ATej9C68EyKzN/8vR5ILxFn6RCdDTKm4asmwZyq2jys12dKmBdWMr9QA=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.23.4", "", { "peerDependencies": { "@tiptap/core": "3.23.4" } }, "sha512-7YjSibNlPcy9eGK+tHt5G/Njr7nPxl+rZ3rCC6TwtLIRLSHPnoGDsfFOgTPkXxaQcE1a/VQwemnYfWc3kdIjDQ=="], @@ -885,12 +992,14 @@ "@tiptap/markdown": ["@tiptap/markdown@3.23.4", "", { "dependencies": { "marked": "17.0.6" }, "peerDependencies": { "@tiptap/core": "3.23.4", "@tiptap/pm": "3.23.4" } }, "sha512-jRh/oa7WyhnXo+vaiaiZ42a5h/m1vvsrEWJHy12vD1qMivRKfNmRJN+lZmYpBV+6h+5vhQpg7EMMIH82xvVWRQ=="], - "@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + "@tiptap/pm": ["@tiptap/pm@3.30.6", "", { "dependencies": { "prosemirror-changeset": "^2.4.1", "prosemirror-commands": "^1.7.1", "prosemirror-dropcursor": "^1.8.2", "prosemirror-gapcursor": "^1.4.1", "prosemirror-history": "^1.5.0", "prosemirror-inputrules": "^1.5.1", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.11", "prosemirror-schema-list": "^1.5.1", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.5", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.9" } }, "sha512-FVzgc+7TMKUreBqFHmyhDSkFUMZPgIzWdeNt4sWLFEzwzdklgKy/+MZClAq9m+bJJU2FKC2+SVmWTatZD6didg=="], "@tiptap/react": ["@tiptap/react@3.23.4", "", { "dependencies": { "@types/use-sync-external-store": "0.0.6", "fast-equals": "5.4.0", "use-sync-external-store": "1.6.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "3.23.4", "@tiptap/extension-floating-menu": "3.23.4" }, "peerDependencies": { "@tiptap/core": "3.23.4", "@tiptap/pm": "3.23.4", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "react": "19.2.6", "react-dom": "19.2.6" } }, "sha512-mb5aIY9PuLreOVLExqs+8BAI20I/8+jCUBfEIqheuFY2GRRuBiwczejSlYuADfVDBbPVN5uPw4UMADCaH5wueQ=="], "@tiptap/starter-kit": ["@tiptap/starter-kit@3.23.4", "", { "dependencies": { "@tiptap/core": "3.23.4", "@tiptap/extension-blockquote": "3.23.4", "@tiptap/extension-bold": "3.23.4", "@tiptap/extension-bullet-list": "3.23.4", "@tiptap/extension-code": "3.23.4", "@tiptap/extension-code-block": "3.23.4", "@tiptap/extension-document": "3.23.4", "@tiptap/extension-dropcursor": "3.23.4", "@tiptap/extension-gapcursor": "3.23.4", "@tiptap/extension-hard-break": "3.23.4", "@tiptap/extension-heading": "3.23.4", "@tiptap/extension-horizontal-rule": "3.23.4", "@tiptap/extension-italic": "3.23.4", "@tiptap/extension-link": "3.23.4", "@tiptap/extension-list": "3.23.4", "@tiptap/extension-list-item": "3.23.4", "@tiptap/extension-list-keymap": "3.23.4", "@tiptap/extension-ordered-list": "3.23.4", "@tiptap/extension-paragraph": "3.23.4", "@tiptap/extension-strike": "3.23.4", "@tiptap/extension-text": "3.23.4", "@tiptap/extension-underline": "3.23.4", "@tiptap/extensions": "3.23.4", "@tiptap/pm": "3.23.4" } }, "sha512-3VhU+NO6/ec9DMj/5Ej0nzARSq42cXnqW+QHCmTL3FNXkXQz+tw1KlfruT5GGJ3M0RssjWjRC0a39N/4S3qxeA=="], + "@tiptap/vue-3": ["@tiptap/vue-3@3.30.6", "", { "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.30.6", "@tiptap/extension-floating-menu": "^3.30.6" }, "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6", "vue": "^3.0.0" } }, "sha512-qSHF1aMjcAKiJrTYbPLVL3bQ1xG4akaf8I/x9/swwEYvwR9OiiNHsZQ5MSwK7hay0vCYMoEsu0sN7xYA7q0o7Q=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "4.4.3", "token-types": "6.1.2" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1053,6 +1162,8 @@ "@types/warning": ["@types/warning@3.0.4", "", {}, "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg=="], + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.4", "", { "dependencies": { "@eslint-community/regexpp": "4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "7.0.5", "natural-compare": "1.4.0", "ts-api-utils": "2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "8.59.4", "eslint": "10.4.0", "typescript": "6.0.3" } }, "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.4", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "4.4.3" }, "peerDependencies": { "eslint": "10.4.0", "typescript": "6.0.3" } }, "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ=="], @@ -1075,6 +1186,10 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + "@unhead/bundler": ["@unhead/bundler@3.4.0", "", { "dependencies": { "magic-string": "^1.1.0", "oxc-walker": "^1.1.1", "unplugin": "^3.3.0" }, "peerDependencies": { "@unhead/cli": "^3.4.0", "@vitejs/devtools-kit": "^0.4.1", "esbuild": ">=0.17.0", "lightningcss": ">=1.20.0", "oxc-parser": ">=0.98.0", "rolldown": ">=1.0.0", "unhead": "^3.4.0", "vite": ">=6.4.2", "webpack": ">=5.0.0" }, "optionalPeers": ["@unhead/cli", "@vitejs/devtools-kit", "esbuild", "lightningcss", "oxc-parser", "rolldown", "vite", "webpack"] }, "sha512-RnsCYynvDiUwmZ/U7TkFwFLr6yTRXVFJlZFj3ehi+m6M4f+GWL/O+6BTQzaHdz59zg2UHHejQvwuXvcL7dj0Dw=="], + + "@unhead/vue": ["@unhead/vue@3.4.0", "", { "dependencies": { "@unhead/bundler": "3.4.0", "hookable": "^6.1.1", "unhead": "3.4.0", "unplugin": "^3.3.0" }, "peerDependencies": { "vite": ">=6.4.2", "vue": ">=3.5.18", "webpack": ">=5.0.0" }, "optionalPeers": ["vite", "webpack"] }, "sha512-fSnDS9d++bqR4t1z6+zFN7bQB5F0+5nwvE4yHW7BGRfEmssCP4fel0GprooUgoLkzx+FcAWMwVIakyaeErSRBA=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.1", "", { "os": "android", "cpu": "arm" }, "sha512-diBxYrhKMJWZiQMFDgKVRDV4zSRyRTR6PBg+0p6/7zAWP6fqUfl0Be0RKvjLhzfRT0Ye5TCAP04gg4rZHSTvnA=="], "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.12.1", "", { "os": "android", "cpu": "arm64" }, "sha512-7VQXkWRrq3zFmL1byHilfy8YjCGxf9dKMYbLIGzR6ujAu4+FB3YD8IkesmpgB9vpiitYjMPs/Dk5Sh/P9aoHLQ=="], @@ -1115,6 +1230,50 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rXHMTryD4YT8wuGDhV8UevKiD02/wUrdKLyokgNQQf/AcO6BCUEkQu5WGQ9i41bA4tlSfKo02WmAcAgxuP6izA=="], + "@vee-validate/zod": ["@vee-validate/zod@4.15.1", "", { "dependencies": { "type-fest": "^4.8.3", "vee-validate": "4.15.1" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.8", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew=="], + + "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], + + "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="], + + "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="], + + "@vue-macros/common": ["@vue-macros/common@3.1.4", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.42", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.42", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.42", "", { "dependencies": { "@vue/compiler-core": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.42", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.42", "@vue/compiler-dom": "3.5.42", "@vue/compiler-ssr": "3.5.42", "@vue/shared": "3.5.42", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.42", "", { "dependencies": { "@vue/compiler-dom": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA=="], + + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@7.7.10", "", { "dependencies": { "@vue/devtools-shared": "^7.7.10", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@7.7.10", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ=="], + + "@vue/language-core": ["@vue/language-core@3.3.11", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.42", "", { "dependencies": { "@vue/shared": "3.5.42" } }, "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.42", "", { "dependencies": { "@vue/reactivity": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.42", "", { "dependencies": { "@vue/reactivity": "3.5.42", "@vue/runtime-core": "3.5.42", "@vue/shared": "3.5.42", "csstype": "^3.2.3" } }, "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.42", "", { "dependencies": { "@vue/compiler-ssr": "3.5.42", "@vue/runtime-dom": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ=="], + + "@vue/shared": ["@vue/shared@3.5.42", "", {}, "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g=="], + + "@vueuse/core": ["@vueuse/core@14.4.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.4.0", "@vueuse/shared": "14.4.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ=="], + + "@vueuse/metadata": ["@vueuse/metadata@14.4.0", "", {}, "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g=="], + + "@vueuse/shared": ["@vueuse/shared@14.4.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g=="], + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], @@ -1179,6 +1338,8 @@ "alce": ["alce@1.2.0", "", { "dependencies": { "esprima": "1.2.5", "estraverse": "1.9.3" } }, "sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w=="], + "alien-signals": ["alien-signals@3.2.1", "", {}, "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "1.1.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -1231,8 +1392,12 @@ "assert-never": ["assert-never@1.4.0", "", {}, "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA=="], + "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + "ast-walker-scope": ["ast-walker-scope@0.9.0", "", { "dependencies": { "@babel/parser": "^7.29.2", "@babel/types": "^7.29.0", "ast-kit": "^2.2.0" } }, "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], @@ -1247,7 +1412,7 @@ "axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], - "axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "1.16.0", "form-data": "4.0.5", "https-proxy-agent": "5.0.1", "proxy-from-env": "2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], + "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -1273,6 +1438,8 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "5.7.1", "inherits": "2.0.4", "readable-stream": "3.6.2" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "1.0.5", "debug": "4.4.3", "http-errors": "2.0.1", "iconv-lite": "0.7.2", "on-finished": "2.4.1", "qs": "6.15.2", "raw-body": "3.0.2", "type-is": "2.1.0" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -1415,10 +1582,14 @@ "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "copy-anything": ["copy-anything@4.1.0", "", {}, "sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "4.1.1", "vary": "1.1.2" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "3.3.1", "js-yaml": "4.1.1", "parse-json": "5.2.0", "path-type": "4.0.0" }, "optionalDependencies": { "typescript": "5.9.3" } }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "countup.js": ["countup.js@2.10.1", "", {}, "sha512-UHW/BsPDgVZfN919D4iu2HPv+jJxZUuaR3EhT0ScFZaD446GiQtkMTgOnOyJXdA2AewdlyUk9UvERlTHTbVpcw=="], + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], "cron": ["cron@4.4.0", "", { "dependencies": { "@types/luxon": "3.7.1", "luxon": "3.7.2" } }, "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ=="], @@ -1761,7 +1932,7 @@ "fork-ts-checker-webpack-plugin": ["fork-ts-checker-webpack-plugin@9.1.0", "", { "dependencies": { "@babel/code-frame": "7.29.0", "chalk": "4.1.2", "chokidar": "4.0.3", "cosmiconfig": "8.3.6", "deepmerge": "4.3.1", "fs-extra": "10.1.0", "memfs": "3.5.3", "minimatch": "3.1.5", "node-abort-controller": "3.1.1", "schema-utils": "3.3.0", "semver": "7.8.0", "tapable": "2.3.3" }, "peerDependencies": { "typescript": "5.9.3", "webpack": "5.106.0" } }, "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.3", "mime-types": "2.1.35" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "2.3.1", "dezalgo": "1.0.4", "once": "1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], @@ -1847,7 +2018,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "1.1.0" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "3.0.4", "@types/unist": "3.0.3", "devlop": "1.1.0", "hastscript": "9.0.1", "property-information": "7.1.0", "vfile": "6.0.3", "vfile-location": "5.0.3", "web-namespaces": "2.0.1" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], @@ -1875,8 +2046,12 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + "hono": ["hono@4.12.20", "", {}, "sha512-mOdYP1JU3//KG4ukutyI5zi2LOjTGMTVwpdaEi+tg2s+Cn2zx8Q1+/b3Xr1f5ynCWophNNTwY6xRzcCTT5W2kA=="], + "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], + "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "0.11.0", "deepmerge": "4.3.1", "dom-serializer": "2.0.0", "htmlparser2": "8.0.2", "selderee": "0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -2095,29 +2270,29 @@ "libqp": ["libqp@2.1.1", "", {}, "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -2137,6 +2312,8 @@ "loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="], + "local-pkg": ["local-pkg@1.2.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], @@ -2185,9 +2362,13 @@ "lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "19.2.6" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="], + "lucide-vue-next": ["lucide-vue-next@1.0.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="], "mailparser": ["mailparser@3.9.8", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", "iconv-lite": "0.7.2", "libmime": "5.3.8", "linkify-it": "5.0.0", "nodemailer": "8.0.5", "punycode.js": "2.3.1", "tlds": "1.261.0" } }, "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w=="], @@ -2197,7 +2378,7 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + "marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -2341,6 +2522,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mjml": ["mjml@5.2.2", "", { "dependencies": { "@babel/runtime": "7.29.2", "mjml-cli": "5.2.2", "mjml-core": "5.2.2", "mjml-preset-core": "5.2.2", "mjml-validator": "5.2.2" }, "bin": { "mjml": "bin/mjml" } }, "sha512-hTxb9kF8CNrzk+1YtjVlwh/8BNlNGozurPn+IbXdGh0GMW+fSXN0CoQK7oRvRrEpsKWjMdXonPCSYZSBNukfDA=="], "mjml-accordion": ["mjml-accordion@5.2.2", "", { "dependencies": { "@babel/runtime": "7.29.2", "lodash": "4.18.1", "mjml-core": "5.2.2" } }, "sha512-QBPlon8CJUSG725DDiE5qGCDUj8wJvsYNwNzQmgqlgZYNAtKvl8nnSAP/TyhZpUmbEmQgh+yKYIeBkR6psX9tg=="], @@ -2403,18 +2586,24 @@ "mjml-wrapper": ["mjml-wrapper@5.2.2", "", { "dependencies": { "@babel/runtime": "7.29.2", "lodash": "4.18.1", "mjml-core": "5.2.2", "mjml-section": "5.2.2" } }, "sha512-oVb5Y0xIWxo1S1xPrzSIElpvmsj0/Xu1GOjW1bv3VWkEvI2L1vU1ebqqnlYBAR+fiVsrCQMHfvZW5FZ/LSbdhA=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "moment-timezone": ["moment-timezone@0.5.48", "", { "dependencies": { "moment": "2.30.1" } }, "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw=="], "motion": ["motion@12.39.0", "", { "dependencies": { "framer-motion": "12.39.0", "tslib": "2.8.1" }, "optionalDependencies": { "react": "19.2.6", "react-dom": "19.2.6" } }, "sha512-H4a+Ze+a9j+/NTla5ezfb/g9vmIOxC+viDj++NGDZyTZkdRKjiOz3kSv6TalRWM8ZmD2y/CfC6TkQc97ybyqSA=="], - "motion-dom": ["motion-dom@12.39.0", "", { "dependencies": { "motion-utils": "12.39.0" } }, "sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ=="], + "motion-dom": ["motion-dom@13.1.1", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA=="], + + "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], - "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + "motion-v": ["motion-v@2.4.0", "", { "dependencies": { "framer-motion": "^13.1.0", "hey-listen": "^1.0.8", "motion-dom": "^13.0.0", "motion-utils": "^13.0.0" }, "peerDependencies": { "@vueuse/core": ">=10.0.0", "vue": ">=3.0.0" } }, "sha512-kRDGMAZk3nvdjEO36Wo6pezSEIStGXGhVFiwo1QkUDsUg8mB5igjYPXyece8wtu2DrHhmFfA6Y1nOz07+5QH4A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + "multer": ["multer@2.1.1", "", { "dependencies": { "append-field": "1.0.0", "busboy": "1.6.0", "concat-stream": "2.0.0", "type-is": "1.6.18" } }, "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A=="], "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], @@ -2455,6 +2644,8 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "nostics": ["nostics@1.2.0", "", {}, "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg=="], + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "3.1.1" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -2507,6 +2698,8 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "1.3.0", "object-keys": "1.1.1", "safe-push-apply": "1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "oxc-walker": ["oxc-walker@1.1.1", "", { "peerDependencies": { "@oxc-project/types": ">=0.98.0", "oxc-parser": ">=0.98.0", "rolldown": ">=1.0.0" }, "optionalPeers": ["@oxc-project/types", "oxc-parser", "rolldown"] }, "sha512-Hwd7dq28zu/Er99+bpWp16CGwFrhyalJh0W3JOB7XpPvgWo3MqMi2ksWGKuzzA9zt50mJ2pIUwR5lpAdZ9ACNQ=="], + "oxlint": ["oxlint@1.78.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.78.0", "@oxlint/binding-android-arm64": "1.78.0", "@oxlint/binding-darwin-arm64": "1.78.0", "@oxlint/binding-darwin-x64": "1.78.0", "@oxlint/binding-freebsd-x64": "1.78.0", "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", "@oxlint/binding-linux-arm-musleabihf": "1.78.0", "@oxlint/binding-linux-arm64-gnu": "1.78.0", "@oxlint/binding-linux-arm64-musl": "1.78.0", "@oxlint/binding-linux-ppc64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-musl": "1.78.0", "@oxlint/binding-linux-s390x-gnu": "1.78.0", "@oxlint/binding-linux-x64-gnu": "1.78.0", "@oxlint/binding-linux-x64-musl": "1.78.0", "@oxlint/binding-openharmony-arm64": "1.78.0", "@oxlint/binding-win32-arm64-msvc": "1.78.0", "@oxlint/binding-win32-ia32-msvc": "1.78.0", "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA=="], "p-event": ["p-event@4.2.0", "", { "dependencies": { "p-timeout": "3.2.0" } }, "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ=="], @@ -2553,6 +2746,8 @@ "passport-strategy": ["passport-strategy@1.0.0", "", {}, "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -2677,6 +2872,8 @@ "posthtml-render": ["posthtml-render@3.0.0", "", { "dependencies": { "is-json": "2.0.1" } }, "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA=="], + "preact": ["preact@10.12.1", "", {}, "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], @@ -2707,9 +2904,11 @@ "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8", "rope-sequence": "1.3.4" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "1.4.4", "w3c-keyname": "2.2.8" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], - "prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + "prosemirror-model": ["prosemirror-model@1.25.11", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ=="], "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], @@ -2719,7 +2918,7 @@ "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "1.25.7" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="], - "prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + "prosemirror-view": ["prosemirror-view@1.42.3", "", { "dependencies": { "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w=="], "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], @@ -2761,6 +2960,8 @@ "qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "random-bytes": ["random-bytes@1.0.0", "", {}, "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="], @@ -2839,6 +3040,8 @@ "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "1.0.9", "@types/hast": "3.0.4", "hast-util-to-estree": "3.1.3" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + "reka-ui": ["reka-ui@2.10.4", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.5", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw=="], + "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "4.0.4", "mdast-util-gfm": "3.1.0", "micromark-extension-gfm": "3.0.0", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -2853,6 +3056,8 @@ "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + "remove-accents": ["remove-accents@0.5.0", "", {}, "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -2875,6 +3080,8 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "4.4.3", "depd": "2.0.0", "is-promise": "4.0.0", "parseurl": "1.3.3", "path-to-regexp": "8.4.2" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -2905,6 +3112,8 @@ "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "7.0.15", "ajv": "6.15.0", "ajv-keywords": "3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "0.12.1" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -2961,6 +3170,8 @@ "speakeasy": ["speakeasy@2.0.0", "", { "dependencies": { "base32.js": "0.0.1" } }, "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw=="], + "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], @@ -3023,6 +3234,8 @@ "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "1.3.1", "cookiejar": "2.1.4", "debug": "4.4.3", "fast-safe-stringify": "2.1.1", "form-data": "4.0.5", "formidable": "3.5.4", "methods": "1.1.2", "mime": "2.6.0", "qs": "6.15.2" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "1.2.2", "methods": "1.1.2", "superagent": "10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3051,7 +3264,7 @@ "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], - "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], @@ -3089,6 +3302,8 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "2.0.0", "media-typer": "1.1.0", "mime-types": "3.0.2" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "is-typed-array": "1.1.15" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -3111,6 +3326,8 @@ "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], "uid": ["uid@2.0.2", "", { "dependencies": { "@lukeed/csprng": "1.1.0" } }, "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g=="], @@ -3129,6 +3346,8 @@ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unhead": ["unhead@3.4.0", "", { "dependencies": { "hookable": "^6.1.1", "unplugin": "^3.3.0" }, "peerDependencies": { "vite": ">=6.4.2" }, "optionalPeers": ["vite"] }, "sha512-OlgpyYjY+NkFyQCaMihg2uTFbbvyr5lZr6TokS68VdFTXJ5jo4JwFtGR8PwKy/wRpjbQYgKF7ZYO+pbRgxtSmw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "3.0.3", "bail": "2.0.2", "devlop": "1.1.0", "extend": "3.0.2", "is-plain-obj": "4.1.0", "trough": "2.2.0", "vfile": "6.0.3" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -3149,6 +3368,10 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unplugin": ["unplugin@3.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.4", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg=="], + + "unplugin-utils": ["unplugin-utils@0.3.2", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.4" } }, "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA=="], + "unrs-resolver": ["unrs-resolver@1.12.1", "", { "dependencies": { "napi-postinstall": "0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.12.1", "@unrs/resolver-binding-android-arm64": "1.12.1", "@unrs/resolver-binding-darwin-arm64": "1.12.1", "@unrs/resolver-binding-darwin-x64": "1.12.1", "@unrs/resolver-binding-freebsd-x64": "1.12.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.12.1", "@unrs/resolver-binding-linux-arm64-musl": "1.12.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.12.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.12.1", "@unrs/resolver-binding-linux-x64-gnu": "1.12.1", "@unrs/resolver-binding-linux-x64-musl": "1.12.1", "@unrs/resolver-binding-openharmony-arm64": "1.12.1", "@unrs/resolver-binding-wasm32-wasi": "1.12.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.12.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.12.1", "@unrs/resolver-binding-win32-x64-msvc": "1.12.1" } }, "sha512-LmOTmcBbFqxu1rzubnqHT6EZeqDYpenlGYwyFhHj7oc1HdyZE+0cLQ+s9SDSK+KKQQKuoJhUbzHQ89Ubwg2Oxg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "3.2.0", "picocolors": "1.1.1" }, "peerDependencies": { "browserslist": "4.28.2" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -3177,6 +3400,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vee-validate": ["vee-validate@4.15.1", "", { "dependencies": { "@vue/devtools-api": "^7.5.2", "type-fest": "^4.8.3" }, "peerDependencies": { "vue": "^3.4.26" } }, "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "3.0.3", "vfile-message": "4.0.3" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "3.0.3", "vfile": "6.0.3" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -3187,8 +3412,22 @@ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "3.2.2", "@types/d3-ease": "3.0.2", "@types/d3-interpolate": "3.0.4", "@types/d3-scale": "4.0.9", "@types/d3-shape": "3.1.8", "@types/d3-time": "3.0.4", "@types/d3-timer": "3.0.2", "d3-array": "3.2.4", "d3-ease": "3.0.1", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", "d3-shape": "3.2.0", "d3-time": "3.1.0", "d3-timer": "3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], + + "vue": ["vue@3.5.42", "", { "dependencies": { "@vue/compiler-dom": "3.5.42", "@vue/compiler-sfc": "3.5.42", "@vue/runtime-dom": "3.5.42", "@vue/server-renderer": "3.5.42", "@vue/shared": "3.5.42" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA=="], + + "vue-chartjs": ["vue-chartjs@5.3.4", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g=="], + + "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], + + "vue-router": ["vue-router@5.3.0", "", { "dependencies": { "@vue-macros/common": "^3.1.3", "@vue/devtools-api": "^8.1.5", "ast-walker-scope": "^0.9.0", "chokidar": "^5.0.0", "confbox": "^0.2.4", "local-pkg": "^1.2.1", "magic-string": "^0.30.21", "mlly": "^1.8.2", "muggle-string": "^0.4.1", "nostics": "^1.1.4", "pathe": "^2.0.3", "picomatch": "^4.0.5", "scule": "^1.3.0", "tinyglobby": "^0.2.17", "unplugin": "^3.3.0", "unplugin-utils": "^0.3.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34 || ^4.0.0", "pinia": "^3.0.4 || ^4.0.2", "vite": "^7.3.0 || ^8.0.0", "vue": "^3.5.34 || ^4.0.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia", "vite"] }, "sha512-a2PBXX9yfkS58JzSJALUffgDa09lEzKmCcEnLaepoIr88L+9hEnsgEQkcadJGID+vtHa0gFpVo064zmylA9fcg=="], + + "vue-tsc": ["vue-tsc@3.3.11", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.11" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "1.4.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], @@ -3205,12 +3444,16 @@ "web-resource-inliner": ["web-resource-inliner@8.0.0", "", { "dependencies": { "ansi-colors": "4.1.3", "escape-goat": "3.0.0", "htmlparser2": "9.1.0", "mime": "2.6.0", "valid-data-url": "3.0.1" } }, "sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA=="], + "web-vue": ["web-vue@workspace:apps/web-vue"], + "webpack": ["webpack@5.106.0", "", { "dependencies": { "@types/eslint-scope": "3.7.7", "@types/estree": "1.0.9", "@types/json-schema": "7.0.15", "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/wasm-edit": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "acorn": "8.16.0", "acorn-import-phases": "1.0.4", "browserslist": "4.28.2", "chrome-trace-event": "1.0.4", "enhanced-resolve": "5.21.4", "es-module-lexer": "2.1.0", "eslint-scope": "5.1.1", "events": "3.3.0", "glob-to-regexp": "0.4.1", "graceful-fs": "4.2.11", "json-parse-even-better-errors": "2.3.1", "loader-runner": "4.3.2", "mime-types": "2.1.35", "neo-async": "2.6.2", "schema-utils": "4.3.3", "tapable": "2.3.3", "terser-webpack-plugin": "5.6.0", "watchpack": "2.5.1", "webpack-sources": "3.4.1" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA=="], "webpack-node-externals": ["webpack-node-externals@3.0.0", "", {}, "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ=="], "webpack-sources": ["webpack-sources@3.4.1", "", {}, "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -3277,6 +3520,8 @@ "@angular-devkit/core/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], + "@angular-devkit/schematics/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "@angular-devkit/schematics/rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], "@angular-devkit/schematics-cli/@inquirer/prompts": ["@inquirer/prompts@7.3.2", "", { "dependencies": { "@inquirer/checkbox": "4.3.2", "@inquirer/confirm": "5.1.21", "@inquirer/editor": "4.2.23", "@inquirer/expand": "4.0.23", "@inquirer/input": "4.3.1", "@inquirer/number": "3.0.23", "@inquirer/password": "4.0.23", "@inquirer/rawlist": "4.1.11", "@inquirer/search": "3.2.2", "@inquirer/select": "4.4.2" }, "optionalDependencies": { "@types/node": "25.9.0" } }, "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ=="], @@ -3395,7 +3640,7 @@ "@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], - "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], @@ -3409,8 +3654,42 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tanstack/vue-query/@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="], + + "@tiptap/core/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-bubble-menu/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-code-block/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-floating-menu/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-horizontal-rule/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-link/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extension-list/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/extensions/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/markdown/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/markdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + + "@tiptap/react/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/starter-kit/@tiptap/pm": ["@tiptap/pm@3.23.4", "", { "dependencies": { "prosemirror-changeset": "2.4.1", "prosemirror-commands": "1.7.1", "prosemirror-dropcursor": "1.8.2", "prosemirror-gapcursor": "1.4.1", "prosemirror-history": "1.5.0", "prosemirror-keymap": "1.2.3", "prosemirror-model": "1.25.7", "prosemirror-schema-list": "1.5.1", "prosemirror-state": "1.4.4", "prosemirror-tables": "1.8.5", "prosemirror-transform": "1.12.0", "prosemirror-view": "1.41.8" } }, "sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ=="], + + "@tiptap/vue-3/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "@tiptap/vue-3/@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.30.6", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-DhfCF2Hkcp6djGpp8yrX9w1lQAzY7oEhW0ouAmWicSJFUrxfhIjrElg/DxMOdHxYlydFeZTZ5vqybBzI34VzCg=="], + + "@tiptap/vue-3/@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.30.6", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-i+xnVadPRFLx7hPVn01UaaSmemC8wPQ64NNFHPD5wOMub006RcZLkmojz4DuAUTBPuArj3i8Sf7ep00Mj2LWbg=="], + "@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@types/superagent/form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.3", "mime-types": "2.1.35" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "@typescript-eslint/eslint-plugin/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -3431,10 +3710,30 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "@typescript-eslint/utils/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@unhead/bundler/magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="], + + "@vee-validate/zod/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + + "@vue/compiler-core/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "@zone-eu/mailsplit/libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="], "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -3461,6 +3760,10 @@ "api-elysia/ioredis": ["ioredis@6.0.0", "", { "dependencies": { "@ioredis/commands": "2.0.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "standard-as-callback": "2.1.0" } }, "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w=="], + "ast-kit/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "ast-walker-scope/@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -3501,6 +3804,12 @@ "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "es-abstract/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "es-set-tostringtag/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "es-shim-unscopables/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "eslint/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -3519,6 +3828,8 @@ "eslint-import-resolver-typescript/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], @@ -3527,14 +3838,20 @@ "eslint-plugin-import/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint-plugin-import/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "eslint-plugin-import/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "0.0.29", "json5": "1.0.2", "minimist": "1.2.8", "strip-bom": "3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], "eslint-plugin-jsx-a11y/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint-plugin-jsx-a11y/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "eslint-plugin-react/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint-plugin-react/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "1.3.0", "is-core-module": "2.16.2", "node-exports-info": "1.6.0", "object-keys": "1.1.1", "path-parse": "1.0.7", "supports-preserve-symlinks-flag": "1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3561,6 +3878,14 @@ "fork-ts-checker-webpack-plugin/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "framer-motion/motion-dom": ["motion-dom@12.39.0", "", { "dependencies": { "motion-utils": "12.39.0" } }, "sha512-Xn7aAcGDhco/JZTXOub64UmaYn73C6J1Po7Fk+8EvkJsNGTqfhon6UJY53vJKXW5v5Zl8HrYsVxv6oPXeGoGLQ=="], + + "framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + + "function.prototype.name/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "html-to-text/htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "2.3.0", "domhandler": "5.0.3", "domutils": "3.2.2", "entities": "4.5.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], @@ -3573,12 +3898,18 @@ "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "internal-slot/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "ioredis/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "is-bun-module/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "is-core-module/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "is-expression/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + "is-regex/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jsonwebtoken/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], @@ -3619,6 +3950,10 @@ "mjml-cli/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "8.0.1", "escalade": "3.2.0", "get-caller-file": "2.0.5", "require-directory": "2.1.1", "string-width": "4.2.3", "y18n": "5.0.8", "yargs-parser": "21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "motion-v/framer-motion": ["framer-motion@13.1.1", "", { "dependencies": { "motion-dom": "^13.1.1", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA=="], + "multer/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "2.1.35" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3637,6 +3972,28 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "prosemirror-commands/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-dropcursor/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "prosemirror-gapcursor/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-gapcursor/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "prosemirror-history/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "prosemirror-schema-list/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-state/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-state/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "prosemirror-tables/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-tables/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "prosemirror-transform/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -3665,6 +4022,8 @@ "superagent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "superagent/form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.3", "mime-types": "2.1.35" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -3675,8 +4034,6 @@ "terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "7.0.15", "ajv": "8.20.0", "ajv-formats": "2.1.1", "ajv-keywords": "5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], - "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "ts-loader/enhanced-resolve": ["enhanced-resolve@5.21.4", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg=="], "ts-loader/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], @@ -3693,10 +4050,34 @@ "typescript-eslint/eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "4.9.1", "@eslint-community/regexpp": "4.12.2", "@eslint/config-array": "0.23.5", "@eslint/config-helpers": "0.6.0", "@eslint/core": "1.2.1", "@eslint/plugin-kit": "0.7.1", "@humanfs/node": "0.16.8", "@humanwhocodes/module-importer": "1.0.1", "@humanwhocodes/retry": "0.4.3", "@types/estree": "1.0.9", "ajv": "6.15.0", "cross-spawn": "7.0.6", "debug": "4.4.3", "escape-string-regexp": "4.0.0", "eslint-scope": "9.1.2", "eslint-visitor-keys": "5.0.1", "espree": "11.2.0", "esquery": "1.7.0", "esutils": "2.0.3", "fast-deep-equal": "3.1.3", "file-entry-cache": "8.0.0", "find-up": "5.0.0", "glob-parent": "6.0.2", "ignore": "5.3.2", "imurmurhash": "0.1.4", "is-glob": "4.0.3", "json-stable-stringify-without-jsonify": "1.0.1", "minimatch": "10.2.5", "natural-compare": "1.4.0", "optionator": "0.9.4" }, "optionalDependencies": { "jiti": "2.7.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "vee-validate/@vue/devtools-api": ["@vue/devtools-api@7.7.10", "", { "dependencies": { "@vue/devtools-kit": "^7.7.10" } }, "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA=="], + + "vue-router/@vue/devtools-api": ["@vue/devtools-api@8.2.1", "", { "dependencies": { "@vue/devtools-kit": "^8.2.1" } }, "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A=="], + + "vue-router/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "web/axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "1.16.0", "form-data": "4.0.5", "https-proxy-agent": "5.0.1", "proxy-from-env": "2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], + "web/prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], "web/prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.8.1", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw=="], + "web-vue/@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-zsAcDEcVIDxxCh+Em0O1yd252gh3T2WmoezPYQOqVIiZwW8yztebjiKMoagFvLyVKZBtHdkW9cYgb62PrR17Pw=="], + + "web-vue/@tiptap/extension-link": ["@tiptap/extension-link@3.30.6", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-xyQ5e3oKphCE84ig8bXCJ6I2esMmFvwHJk4/o8uIxQBVT1yOUyG3ZNJG0431SQ2xrUw0bPWY80F9Kla7HFiSgA=="], + + "web-vue/@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.30.6", "", { "peerDependencies": { "@tiptap/extensions": "3.30.6" } }, "sha512-mDW1u45YsTsFexq2EZAKTLPU0koX+FDL5iZGztet7IlNWJx8tKL6jqmeRTlCHV8PfKzZZNza0pNVpT/0DdWycQ=="], + + "web-vue/@tiptap/markdown": ["@tiptap/markdown@3.30.6", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-TfWxG+a26DBoqGpkSEBYuGHOxnW0n1n6f6BnrnrSip2pczR1zSgYwzx0T8bwMoSyGxqeubMPjOje+PDiHiCUWQ=="], + + "web-vue/@tiptap/starter-kit": ["@tiptap/starter-kit@3.30.6", "", { "dependencies": { "@tiptap/core": "3.30.6", "@tiptap/extension-blockquote": "3.30.6", "@tiptap/extension-bold": "3.30.6", "@tiptap/extension-bullet-list": "3.30.6", "@tiptap/extension-code": "3.30.6", "@tiptap/extension-code-block": "3.30.6", "@tiptap/extension-document": "3.30.6", "@tiptap/extension-dropcursor": "3.30.6", "@tiptap/extension-gapcursor": "3.30.6", "@tiptap/extension-hard-break": "3.30.6", "@tiptap/extension-heading": "3.30.6", "@tiptap/extension-horizontal-rule": "3.30.6", "@tiptap/extension-italic": "3.30.6", "@tiptap/extension-link": "3.30.6", "@tiptap/extension-list": "3.30.6", "@tiptap/extension-list-item": "3.30.6", "@tiptap/extension-list-keymap": "3.30.6", "@tiptap/extension-ordered-list": "3.30.6", "@tiptap/extension-paragraph": "3.30.6", "@tiptap/extension-strike": "3.30.6", "@tiptap/extension-text": "3.30.6", "@tiptap/extension-underline": "3.30.6", "@tiptap/extensions": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-qQ+awpHaTNYhYbAAxLavWAo0CJBq43m1088yCxHhsITT1b2gGI0Mp2tTJ4aWtm0GxaJIlERUEvHfZRj/HuOJRg=="], + + "web-vue/@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], + + "web-vue/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "web-vue/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + "webpack/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "webpack/enhanced-resolve": ["enhanced-resolve@5.21.4", "", { "dependencies": { "graceful-fs": "4.2.11", "tapable": "2.3.3" } }, "sha512-wE4fDO8OjJhrPFH69HUQStq5oKvGRTNXEyW+k5C/pUQLASSsTu7obd2V3GvCDgPcY9AWjhJ4jz9Kh7iRvrxhJg=="], @@ -3765,10 +4146,78 @@ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "optionalDependencies": { "@types/react": "19.2.14" }, "peerDependencies": { "react": "19.2.6" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@tiptap/core/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/core/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-bubble-menu/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-bubble-menu/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-code-block/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-code-block/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-floating-menu/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-floating-menu/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-horizontal-rule/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-horizontal-rule/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-link/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-link/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extension-list/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extension-list/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/extensions/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/extensions/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/markdown/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/markdown/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/react/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/react/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@tiptap/starter-kit/@tiptap/pm/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "@tiptap/starter-kit/@tiptap/pm/prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "1.25.7", "prosemirror-state": "1.4.4", "prosemirror-transform": "1.12.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "@types/superagent/form-data/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "@typescript-eslint/eslint-plugin/eslint/@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "3.0.5", "debug": "4.4.3", "minimatch": "10.2.5" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], "@typescript-eslint/eslint-plugin/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], @@ -3827,6 +4276,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@typescript-eslint/utils/eslint/@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "3.0.5", "debug": "4.4.3", "minimatch": "10.2.5" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], "@typescript-eslint/utils/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], @@ -3847,6 +4298,10 @@ "@typescript-eslint/utils/eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@vue/compiler-core/@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@vue/compiler-sfc/@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "@zone-eu/mailsplit/libmime/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -3861,6 +4316,10 @@ "api-elysia/ioredis/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "ast-kit/@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "ast-walker-scope/@babel/parser/@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -3929,6 +4388,8 @@ "eslint-import-resolver-typescript/eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "eslint-import-resolver-typescript/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "eslint-module-utils/eslint/@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "3.0.5", "debug": "4.4.3", "minimatch": "10.2.5" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], "eslint-module-utils/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], @@ -4063,6 +4524,8 @@ "mjml-cli/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "multer/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], "posthtml-parser/htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "2.3.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], @@ -4071,8 +4534,14 @@ "posthtml-parser/htmlparser2/entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], + "prosemirror-dropcursor/prosemirror-view/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-history/prosemirror-view/prosemirror-model": ["prosemirror-model@1.25.7", "", { "dependencies": { "orderedmap": "2.1.1" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "superagent/form-data/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "terser-webpack-plugin/schema-utils/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-uri": "3.1.2", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "terser-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "optionalDependencies": { "ajv": "8.20.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], @@ -4101,6 +4570,66 @@ "typescript-eslint/eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "5.0.6" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.2.1", "", { "dependencies": { "@vue/devtools-shared": "^8.2.1", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ=="], + + "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "web-vue/@tiptap/extension-code-block/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "web-vue/@tiptap/extension-link/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "web-vue/@tiptap/extension-placeholder/@tiptap/extensions": ["@tiptap/extensions@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-8SYpu3E+ZuWhTGWtjcWPtcJmcotrADYBuXIu+bgdZjcuaxRHs+6IpNvdLabl3q46W7+0A11GginPDJNqrMKk2A=="], + + "web-vue/@tiptap/markdown/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "web-vue/@tiptap/markdown/marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-tOBZ1Ol7zWCAlsuHK76cWoMMJyyAhJG+cZn4iFRtmGQZGyZUQMYYlSahgvxbDLFdcuB977nuUNAI2XniEKA6cQ=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-bold": ["@tiptap/extension-bold@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-vQSqHcnoe5K9f5/XpWH8tBDvrFI3uvEv17qFj28dlNH/DOIMHPSBSFKzrfinuFcDGHHb1B1pTE5Yu0KH2Qomow=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.30.6", "", { "peerDependencies": { "@tiptap/extension-list": "3.30.6" } }, "sha512-rVPEN4fZqwDcNrxfxSgPkoK4gPbwVLgwEJWvb9Y3Nhf4SkwrKDvO05QHTa6+DGe5LP3Tl5dC305l5UsmnwfHqg=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-code": ["@tiptap/extension-code@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-az8RByq9Aftg+AwrQ2fS08o4SJZ9VIwJNYngwDwAGY8OxM1z/tPVTZRz771XV6V+ngbfh7J9Rv3Gk4MNmNUp0w=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-document": ["@tiptap/extension-document@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-eaDIPRJNAzc3hpRizrySColkdxXDBtrFhZHRU3TZv2O6HVKmg3LVSSCpJiHSxY6iR8+TC41p4bg8Ql6O20Y9xQ=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.30.6", "", { "peerDependencies": { "@tiptap/extensions": "3.30.6" } }, "sha512-GDuhDsUxIIiluLS1RuZ9zoPqHdybqJpuV6VhCGFJuzDmiraYPctj3AbbkX9xYZyukgO6tPN9MQtX26LJuFt7og=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.30.6", "", { "peerDependencies": { "@tiptap/extensions": "3.30.6" } }, "sha512-QDEip3y0k2W2oMhNc1Yyi1NhD+4Q+BtCjuCRpRld9VH60cuRROIq60KU+VkOoCJ9o6p6djrn6j1g6PZsG69Bxw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-8ALo10VWFYLF/9eS3iRZrx3EMbokMhRdJbSgk9+dU+zuh8yGFQnMyf30Wpu3K9DIseHtDZQTI7rjl0o8ayNPZw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-heading": ["@tiptap/extension-heading@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-32lPwP8BeLFGQV+U/HrwbK+ItQMiN13LppKnQtUUNl3FwG440qPpnQo7eCEAxTkUg/M+VTjNn/UHnJMsOFPklw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-38SokgorOsmLNWwMHhrkoxIaWd73Setoo4J0slrlqvMItovshqjqYjtSohdUk09Yr+cxxcGUP9m8zxBEyi5ZsA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-italic": ["@tiptap/extension-italic@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-vbltMPEePHWsGhvPH01yfT9GNJNMs2oUwPhqC0z/Brychu/oFe+0FR4RKFBVXnZg8oRfGQEOJ6OjD9jKDAZ0zQ=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-list": ["@tiptap/extension-list@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-vNzXMap27plACDvyeEUlUyB01Rw1IOWn+wABIEuiIyJzVpZ6n5T8ck2nwHXp6lTy5xxGi7MnVq8epOjqCG1g8A=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.30.6", "", { "peerDependencies": { "@tiptap/extension-list": "3.30.6" } }, "sha512-a97knOHxLI/QF1nvtn6SIC9abspB7miKXBSXMGQwlt5iqJcwHDyj1neXi2KAruam8i4932uc+zishYeFka9yvA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.30.6", "", { "peerDependencies": { "@tiptap/extension-list": "3.30.6" } }, "sha512-FTNFPR+DYDWR48Ak46tvpNzHPQ1exUgxFVUv4grHJq8tdMl1KWYW4DZmcOIe2Tk7kmlDmC14rKmUvFcdKMUgAA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.30.6", "", { "peerDependencies": { "@tiptap/extension-list": "3.30.6" } }, "sha512-DWQELBaaCtL9ZRdkeIjgNsz0wZ93SAdPVBpH0pIxOsf/p2JbSBI1ipCVGVysr9SB8ULGsCthZk+cZwZoAOu7qA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-l+syX39aTG3BSNmkIXgrawRgSrFmQ/Hlz+5j2+OYXcHFFqj2cnnpZMzZFMynJUFKRLDgldfUs+UMsgelZNDScw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-strike": ["@tiptap/extension-strike@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-aRtyDDS88+eT6r3krn94ajLTVccxaukEHWJoDwq0devAqFq9d0ftaYU9aNSSKzG/TAsIObXX/1MKSiJQC+Z/Rw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-text": ["@tiptap/extension-text@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-N55Mf9Xo3Zl5f7MSlBpxIYWktYIgE7yY3QT9IGrhm6VBSF9bAHToVDOEWCkgTEinerBcWraPFMDl8NMtX7Cdqw=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extension-underline": ["@tiptap/extension-underline@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6" } }, "sha512-6NyWmSOGzbeiwT6Vita3F/msePEQ2fuuzjSguPU1ebXUG04D3h8rNjoBrrfGt6OrwP/pdIri3VVVP5H1suxxkA=="], + + "web-vue/@tiptap/starter-kit/@tiptap/extensions": ["@tiptap/extensions@3.30.6", "", { "peerDependencies": { "@tiptap/core": "3.30.6", "@tiptap/pm": "3.30.6" } }, "sha512-8SYpu3E+ZuWhTGWtjcWPtcJmcotrADYBuXIu+bgdZjcuaxRHs+6IpNvdLabl3q46W7+0A11GginPDJNqrMKk2A=="], + + "web-vue/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "web/axios/form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.3", "mime-types": "2.1.35" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], "webpack/schema-utils/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-uri": "3.1.2", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -4147,6 +4676,22 @@ "@typescript-eslint/utils/eslint/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@vue/compiler-core/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@vue/compiler-sfc/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@vue/compiler-sfc/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "ast-kit/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "ast-kit/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "ast-walker-scope/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "ast-walker-scope/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "eslint-config-next/eslint/@eslint/config-array/@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], @@ -4219,6 +4764,14 @@ "typescript-eslint/eslint/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "4.0.4" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + "vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.2.1", "", {}, "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g=="], + + "vue-router/@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "web-vue/@tiptap/extension-placeholder/@tiptap/extensions/@tiptap/core": ["@tiptap/core@3.30.6", "", { "peerDependencies": { "@tiptap/pm": "3.30.6" } }, "sha512-M32+WOx1IrPRbPunhG1Xl7ETgUZJGABpGi/y969Uio6rR4tPBnOgFG5xXE3Xw6OQtYUqUE2n9nnnckoObZwBzA=="], + + "web/axios/form-data/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "2.3.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], From 0a348b53c27e8f24695821185490ca9df1fee4f1 Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 04:16:09 +0700 Subject: [PATCH 03/31] feat(web-vue): port 12 ui components to Vue (reka-ui/motion-v/chart.js/tiptap) (#41) --- .../src/components/ui/animated-beam.vue | 138 ++++ apps/web-vue/src/components/ui/avatar.vue | 30 + apps/web-vue/src/components/ui/bento-grid.vue | 121 ++++ apps/web-vue/src/components/ui/button.vue | 64 ++ apps/web-vue/src/components/ui/card.vue | 33 + apps/web-vue/src/components/ui/chart.vue | 342 +++++++++ .../src/components/ui/dropdown-menu.vue | 176 +++++ apps/web-vue/src/components/ui/input.vue | 20 + .../src/components/ui/markdown-editor.ts | 2 + .../src/components/ui/markdown-editor.vue | 661 ++++++++++++++++++ .../src/components/ui/navigation-menu.vue | 117 ++++ .../src/components/ui/number-ticker.vue | 59 ++ apps/web-vue/src/components/ui/table.vue | 47 ++ apps/web-vue/src/pages/ui-smoke.vue | 130 ++++ 14 files changed, 1940 insertions(+) create mode 100644 apps/web-vue/src/components/ui/animated-beam.vue create mode 100644 apps/web-vue/src/components/ui/avatar.vue create mode 100644 apps/web-vue/src/components/ui/bento-grid.vue create mode 100644 apps/web-vue/src/components/ui/button.vue create mode 100644 apps/web-vue/src/components/ui/card.vue create mode 100644 apps/web-vue/src/components/ui/chart.vue create mode 100644 apps/web-vue/src/components/ui/dropdown-menu.vue create mode 100644 apps/web-vue/src/components/ui/input.vue create mode 100644 apps/web-vue/src/components/ui/markdown-editor.ts create mode 100644 apps/web-vue/src/components/ui/markdown-editor.vue create mode 100644 apps/web-vue/src/components/ui/navigation-menu.vue create mode 100644 apps/web-vue/src/components/ui/number-ticker.vue create mode 100644 apps/web-vue/src/components/ui/table.vue create mode 100644 apps/web-vue/src/pages/ui-smoke.vue diff --git a/apps/web-vue/src/components/ui/animated-beam.vue b/apps/web-vue/src/components/ui/animated-beam.vue new file mode 100644 index 0000000..a7bc8c1 --- /dev/null +++ b/apps/web-vue/src/components/ui/animated-beam.vue @@ -0,0 +1,138 @@ + diff --git a/apps/web-vue/src/components/ui/avatar.vue b/apps/web-vue/src/components/ui/avatar.vue new file mode 100644 index 0000000..1664c64 --- /dev/null +++ b/apps/web-vue/src/components/ui/avatar.vue @@ -0,0 +1,30 @@ + diff --git a/apps/web-vue/src/components/ui/bento-grid.vue b/apps/web-vue/src/components/ui/bento-grid.vue new file mode 100644 index 0000000..06ad976 --- /dev/null +++ b/apps/web-vue/src/components/ui/bento-grid.vue @@ -0,0 +1,121 @@ + diff --git a/apps/web-vue/src/components/ui/button.vue b/apps/web-vue/src/components/ui/button.vue new file mode 100644 index 0000000..efd46b4 --- /dev/null +++ b/apps/web-vue/src/components/ui/button.vue @@ -0,0 +1,64 @@ + diff --git a/apps/web-vue/src/components/ui/card.vue b/apps/web-vue/src/components/ui/card.vue new file mode 100644 index 0000000..51f1048 --- /dev/null +++ b/apps/web-vue/src/components/ui/card.vue @@ -0,0 +1,33 @@ + diff --git a/apps/web-vue/src/components/ui/chart.vue b/apps/web-vue/src/components/ui/chart.vue new file mode 100644 index 0000000..d4bff3a --- /dev/null +++ b/apps/web-vue/src/components/ui/chart.vue @@ -0,0 +1,342 @@ + diff --git a/apps/web-vue/src/components/ui/dropdown-menu.vue b/apps/web-vue/src/components/ui/dropdown-menu.vue new file mode 100644 index 0000000..7162a66 --- /dev/null +++ b/apps/web-vue/src/components/ui/dropdown-menu.vue @@ -0,0 +1,176 @@ + diff --git a/apps/web-vue/src/components/ui/input.vue b/apps/web-vue/src/components/ui/input.vue new file mode 100644 index 0000000..f6edb44 --- /dev/null +++ b/apps/web-vue/src/components/ui/input.vue @@ -0,0 +1,20 @@ + diff --git a/apps/web-vue/src/components/ui/markdown-editor.ts b/apps/web-vue/src/components/ui/markdown-editor.ts new file mode 100644 index 0000000..8e6aec1 --- /dev/null +++ b/apps/web-vue/src/components/ui/markdown-editor.ts @@ -0,0 +1,2 @@ +export { default as MarkdownEditor, default } from './markdown-editor.vue'; +export type { MarkdownEditorTheme } from './markdown-editor.vue'; diff --git a/apps/web-vue/src/components/ui/markdown-editor.vue b/apps/web-vue/src/components/ui/markdown-editor.vue new file mode 100644 index 0000000..7b78426 --- /dev/null +++ b/apps/web-vue/src/components/ui/markdown-editor.vue @@ -0,0 +1,661 @@ + + + + + diff --git a/apps/web-vue/src/components/ui/navigation-menu.vue b/apps/web-vue/src/components/ui/navigation-menu.vue new file mode 100644 index 0000000..ebbeb1e --- /dev/null +++ b/apps/web-vue/src/components/ui/navigation-menu.vue @@ -0,0 +1,117 @@ + diff --git a/apps/web-vue/src/components/ui/number-ticker.vue b/apps/web-vue/src/components/ui/number-ticker.vue new file mode 100644 index 0000000..c6d52c8 --- /dev/null +++ b/apps/web-vue/src/components/ui/number-ticker.vue @@ -0,0 +1,59 @@ + diff --git a/apps/web-vue/src/components/ui/table.vue b/apps/web-vue/src/components/ui/table.vue new file mode 100644 index 0000000..e3264e8 --- /dev/null +++ b/apps/web-vue/src/components/ui/table.vue @@ -0,0 +1,47 @@ + diff --git a/apps/web-vue/src/pages/ui-smoke.vue b/apps/web-vue/src/pages/ui-smoke.vue new file mode 100644 index 0000000..f147d37 --- /dev/null +++ b/apps/web-vue/src/pages/ui-smoke.vue @@ -0,0 +1,130 @@ + + + From 0086eef0d17d2966a18172783a95c51cae13c1cd Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 04:45:21 +0700 Subject: [PATCH 04/31] =?UTF-8?q?feat(web-vue):=20app=20shell=20=E2=80=94?= =?UTF-8?q?=20port=20lib/composables/router=20with=20all=2042=20stub=20rou?= =?UTF-8?q?tes=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-vue/public/image/wds-logo.svg | 9 + apps/web-vue/src/App.vue | 34 +- .../src/components/CsrfInitializer.vue | 17 + apps/web-vue/src/components/Footer.vue | 107 ++++++ apps/web-vue/src/components/Navbar.vue | 218 +++++++++++ .../src/components/wds/user-avatar.vue | 176 +++++++++ apps/web-vue/src/composables/use-auth.ts | 123 ++++++ .../src/composables/use-cart-drawer.ts | 20 + apps/web-vue/src/data/footer.ts | 98 +++++ apps/web-vue/src/lib/api-client.ts | 290 ++++++++++++++ apps/web-vue/src/lib/api/admin.ts | 207 ++++++++++ apps/web-vue/src/lib/api/auth.ts | 130 +++++++ apps/web-vue/src/lib/api/blog.ts | 185 +++++++++ apps/web-vue/src/lib/api/cart.ts | 97 +++++ apps/web-vue/src/lib/api/events.ts | 50 +++ apps/web-vue/src/lib/api/hooks/use-auth.ts | 356 ++++++++++++++++++ apps/web-vue/src/lib/api/hooks/use-cart.ts | 230 +++++++++++ apps/web-vue/src/lib/api/hooks/use-events.ts | 45 +++ apps/web-vue/src/lib/api/hooks/use-orders.ts | 115 ++++++ .../web-vue/src/lib/api/hooks/use-payments.ts | 25 ++ .../web-vue/src/lib/api/hooks/use-products.ts | 54 +++ apps/web-vue/src/lib/api/hooks/use-reviews.ts | 96 +++++ .../web-vue/src/lib/api/hooks/use-settings.ts | 41 ++ apps/web-vue/src/lib/api/hooks/use-user.ts | 63 ++++ apps/web-vue/src/lib/api/orders.ts | 140 +++++++ apps/web-vue/src/lib/api/payments.ts | 49 +++ apps/web-vue/src/lib/api/products.ts | 100 +++++ apps/web-vue/src/lib/api/reviews.ts | 106 ++++++ apps/web-vue/src/lib/api/users.ts | 82 ++++ apps/web-vue/src/lib/csrf.ts | 79 ++++ apps/web-vue/src/lib/events/mock-events.ts | 274 ++++++++++++++ apps/web-vue/src/lib/events/types.ts | 43 +++ apps/web-vue/src/lib/hooks/use-debounce.ts | 45 +++ apps/web-vue/src/lib/metadata.ts | 118 ++++++ apps/web-vue/src/lib/product-slug-mapping.ts | 42 +++ .../web-vue/src/lib/product-static-content.ts | 137 +++++++ apps/web-vue/src/lib/query-client.ts | 14 + apps/web-vue/src/lib/shipping.ts | 33 ++ apps/web-vue/src/lib/structured-data.ts | 154 ++++++++ apps/web-vue/src/lib/toast.ts | 28 ++ apps/web-vue/src/lib/utils.ts | 10 + apps/web-vue/src/lib/utils/avatar.ts | 41 ++ apps/web-vue/src/main.ts | 5 +- apps/web-vue/src/pages/Home.vue | 5 - apps/web-vue/src/pages/about.vue | 6 + apps/web-vue/src/pages/account/profile.vue | 6 + apps/web-vue/src/pages/account/settings.vue | 6 + apps/web-vue/src/pages/achievements.vue | 6 + apps/web-vue/src/pages/activities.vue | 6 + apps/web-vue/src/pages/admin/blog/[id].vue | 6 + apps/web-vue/src/pages/admin/blog/index.vue | 6 + apps/web-vue/src/pages/admin/blog/new.vue | 6 + apps/web-vue/src/pages/admin/index.vue | 6 + apps/web-vue/src/pages/admin/orders.vue | 6 + apps/web-vue/src/pages/admin/products.vue | 6 + apps/web-vue/src/pages/admin/transactions.vue | 6 + apps/web-vue/src/pages/admin/users.vue | 6 + apps/web-vue/src/pages/auth/2fa.vue | 6 + .../src/pages/auth/forgot-password.vue | 6 + apps/web-vue/src/pages/auth/login.vue | 6 + .../web-vue/src/pages/auth/oauth-callback.vue | 6 + .../web-vue/src/pages/auth/reset-password.vue | 6 + apps/web-vue/src/pages/auth/signup.vue | 6 + apps/web-vue/src/pages/auth/verify-email.vue | 6 + apps/web-vue/src/pages/blog/[slug].vue | 6 + apps/web-vue/src/pages/blog/index.vue | 6 + apps/web-vue/src/pages/calendar.vue | 6 + apps/web-vue/src/pages/cart.vue | 6 + apps/web-vue/src/pages/checkout.vue | 6 + apps/web-vue/src/pages/faq.vue | 6 + apps/web-vue/src/pages/generation.vue | 6 + apps/web-vue/src/pages/home.vue | 6 + apps/web-vue/src/pages/legal/privacy.vue | 6 + apps/web-vue/src/pages/legal/refund.vue | 6 + apps/web-vue/src/pages/legal/terms.vue | 6 + apps/web-vue/src/pages/not-found.vue | 6 + apps/web-vue/src/pages/orders/[id].vue | 6 + apps/web-vue/src/pages/orders/index.vue | 6 + apps/web-vue/src/pages/partner.vue | 6 + apps/web-vue/src/pages/payments/[orderId].vue | 6 + apps/web-vue/src/pages/payments/cancel.vue | 6 + apps/web-vue/src/pages/payments/return.vue | 6 + apps/web-vue/src/pages/shop/ao-thun.vue | 6 + apps/web-vue/src/pages/shop/day-deo.vue | 6 + apps/web-vue/src/pages/shop/index.vue | 6 + apps/web-vue/src/pages/shop/moc-khoa.vue | 6 + apps/web-vue/src/pages/shop/pad-chuot.vue | 6 + apps/web-vue/src/router/index.ts | 137 ++++++- apps/web-vue/src/types/auth.types.ts | 136 +++++++ apps/web-vue/src/types/product.ts | 63 ++++ 90 files changed, 4873 insertions(+), 12 deletions(-) create mode 100644 apps/web-vue/public/image/wds-logo.svg create mode 100644 apps/web-vue/src/components/CsrfInitializer.vue create mode 100644 apps/web-vue/src/components/Footer.vue create mode 100644 apps/web-vue/src/components/Navbar.vue create mode 100644 apps/web-vue/src/components/wds/user-avatar.vue create mode 100644 apps/web-vue/src/composables/use-auth.ts create mode 100644 apps/web-vue/src/composables/use-cart-drawer.ts create mode 100644 apps/web-vue/src/data/footer.ts create mode 100644 apps/web-vue/src/lib/api-client.ts create mode 100644 apps/web-vue/src/lib/api/admin.ts create mode 100644 apps/web-vue/src/lib/api/auth.ts create mode 100644 apps/web-vue/src/lib/api/blog.ts create mode 100644 apps/web-vue/src/lib/api/cart.ts create mode 100644 apps/web-vue/src/lib/api/events.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-auth.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-cart.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-events.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-orders.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-payments.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-products.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-reviews.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-settings.ts create mode 100644 apps/web-vue/src/lib/api/hooks/use-user.ts create mode 100644 apps/web-vue/src/lib/api/orders.ts create mode 100644 apps/web-vue/src/lib/api/payments.ts create mode 100644 apps/web-vue/src/lib/api/products.ts create mode 100644 apps/web-vue/src/lib/api/reviews.ts create mode 100644 apps/web-vue/src/lib/api/users.ts create mode 100644 apps/web-vue/src/lib/csrf.ts create mode 100644 apps/web-vue/src/lib/events/mock-events.ts create mode 100644 apps/web-vue/src/lib/events/types.ts create mode 100644 apps/web-vue/src/lib/hooks/use-debounce.ts create mode 100644 apps/web-vue/src/lib/metadata.ts create mode 100644 apps/web-vue/src/lib/product-slug-mapping.ts create mode 100644 apps/web-vue/src/lib/product-static-content.ts create mode 100644 apps/web-vue/src/lib/query-client.ts create mode 100644 apps/web-vue/src/lib/shipping.ts create mode 100644 apps/web-vue/src/lib/structured-data.ts create mode 100644 apps/web-vue/src/lib/toast.ts create mode 100644 apps/web-vue/src/lib/utils.ts create mode 100644 apps/web-vue/src/lib/utils/avatar.ts delete mode 100644 apps/web-vue/src/pages/Home.vue create mode 100644 apps/web-vue/src/pages/about.vue create mode 100644 apps/web-vue/src/pages/account/profile.vue create mode 100644 apps/web-vue/src/pages/account/settings.vue create mode 100644 apps/web-vue/src/pages/achievements.vue create mode 100644 apps/web-vue/src/pages/activities.vue create mode 100644 apps/web-vue/src/pages/admin/blog/[id].vue create mode 100644 apps/web-vue/src/pages/admin/blog/index.vue create mode 100644 apps/web-vue/src/pages/admin/blog/new.vue create mode 100644 apps/web-vue/src/pages/admin/index.vue create mode 100644 apps/web-vue/src/pages/admin/orders.vue create mode 100644 apps/web-vue/src/pages/admin/products.vue create mode 100644 apps/web-vue/src/pages/admin/transactions.vue create mode 100644 apps/web-vue/src/pages/admin/users.vue create mode 100644 apps/web-vue/src/pages/auth/2fa.vue create mode 100644 apps/web-vue/src/pages/auth/forgot-password.vue create mode 100644 apps/web-vue/src/pages/auth/login.vue create mode 100644 apps/web-vue/src/pages/auth/oauth-callback.vue create mode 100644 apps/web-vue/src/pages/auth/reset-password.vue create mode 100644 apps/web-vue/src/pages/auth/signup.vue create mode 100644 apps/web-vue/src/pages/auth/verify-email.vue create mode 100644 apps/web-vue/src/pages/blog/[slug].vue create mode 100644 apps/web-vue/src/pages/blog/index.vue create mode 100644 apps/web-vue/src/pages/calendar.vue create mode 100644 apps/web-vue/src/pages/cart.vue create mode 100644 apps/web-vue/src/pages/checkout.vue create mode 100644 apps/web-vue/src/pages/faq.vue create mode 100644 apps/web-vue/src/pages/generation.vue create mode 100644 apps/web-vue/src/pages/home.vue create mode 100644 apps/web-vue/src/pages/legal/privacy.vue create mode 100644 apps/web-vue/src/pages/legal/refund.vue create mode 100644 apps/web-vue/src/pages/legal/terms.vue create mode 100644 apps/web-vue/src/pages/not-found.vue create mode 100644 apps/web-vue/src/pages/orders/[id].vue create mode 100644 apps/web-vue/src/pages/orders/index.vue create mode 100644 apps/web-vue/src/pages/partner.vue create mode 100644 apps/web-vue/src/pages/payments/[orderId].vue create mode 100644 apps/web-vue/src/pages/payments/cancel.vue create mode 100644 apps/web-vue/src/pages/payments/return.vue create mode 100644 apps/web-vue/src/pages/shop/ao-thun.vue create mode 100644 apps/web-vue/src/pages/shop/day-deo.vue create mode 100644 apps/web-vue/src/pages/shop/index.vue create mode 100644 apps/web-vue/src/pages/shop/moc-khoa.vue create mode 100644 apps/web-vue/src/pages/shop/pad-chuot.vue create mode 100644 apps/web-vue/src/types/auth.types.ts create mode 100644 apps/web-vue/src/types/product.ts diff --git a/apps/web-vue/public/image/wds-logo.svg b/apps/web-vue/public/image/wds-logo.svg new file mode 100644 index 0000000..5b0a59a --- /dev/null +++ b/apps/web-vue/public/image/wds-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web-vue/src/App.vue b/apps/web-vue/src/App.vue index 613fac0..2232018 100644 --- a/apps/web-vue/src/App.vue +++ b/apps/web-vue/src/App.vue @@ -1,5 +1,35 @@ - + diff --git a/apps/web-vue/src/components/CsrfInitializer.vue b/apps/web-vue/src/components/CsrfInitializer.vue new file mode 100644 index 0000000..bb6593c --- /dev/null +++ b/apps/web-vue/src/components/CsrfInitializer.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/web-vue/src/components/Footer.vue b/apps/web-vue/src/components/Footer.vue new file mode 100644 index 0000000..11aa422 --- /dev/null +++ b/apps/web-vue/src/components/Footer.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/web-vue/src/components/Navbar.vue b/apps/web-vue/src/components/Navbar.vue new file mode 100644 index 0000000..66f56e8 --- /dev/null +++ b/apps/web-vue/src/components/Navbar.vue @@ -0,0 +1,218 @@ + + + diff --git a/apps/web-vue/src/components/wds/user-avatar.vue b/apps/web-vue/src/components/wds/user-avatar.vue new file mode 100644 index 0000000..57b59d0 --- /dev/null +++ b/apps/web-vue/src/components/wds/user-avatar.vue @@ -0,0 +1,176 @@ + + + diff --git a/apps/web-vue/src/composables/use-auth.ts b/apps/web-vue/src/composables/use-auth.ts new file mode 100644 index 0000000..62e672d --- /dev/null +++ b/apps/web-vue/src/composables/use-auth.ts @@ -0,0 +1,123 @@ +// Port of apps/web auth.context.tsx + hooks/use-auth.ts: state comes from the cached +// useCurrentUser query; App.vue calls provideAuth() once, descendants useAuth() via inject, +// and the router guard (outside any component) uses authStoreReady()/the module fallback. +import { + computed, + inject, + provide, + ref, + watch, + type ComputedRef, + type InjectionKey, + type Ref, +} from 'vue'; + +import { + useCurrentUser, + useLogin, + useLogout, + useRegister, + useVerifyEmail, +} from '@/lib/api/hooks/use-auth'; +import type { LoginRequest, LoginResponse, RegisterRequest, User } from '@/types/auth.types'; + +export interface AuthStore { + user: ComputedRef; + isAuthenticated: ComputedRef; + isLoading: ComputedRef; + requires2FA: Ref; + login: (data: LoginRequest) => Promise; + register: (data: RegisterRequest) => Promise; + logout: (sessionId?: string) => Promise; + verifyEmail: (token: string) => Promise; + refreshUser: () => Promise; + isLoggingIn: ComputedRef; + isRegistering: ComputedRef; + isLoggingOut: ComputedRef; + isVerifyingEmail: ComputedRef; + /** Resolves once the initial current-user fetch has settled (guard waits on this). */ + whenLoaded: () => Promise; +} + +const AuthKey: InjectionKey = Symbol('auth'); + +let fallback: AuthStore | null = null; +let resolveStore: (store: AuthStore) => void = () => {}; +const storeReady = new Promise((resolve) => { + resolveStore = resolve; +}); + +export function provideAuth(): AuthStore { + const currentUser = useCurrentUser(); + const loginMutation = useLogin(); + const registerMutation = useRegister(); + const logoutMutation = useLogout(); + const verifyEmailMutation = useVerifyEmail(); + + const requires2FA = ref(false); + + const user = computed(() => currentUser.data.value); + + let loadedPromise: Promise | null = null; + function whenLoaded() { + if (!loadedPromise) { + loadedPromise = new Promise((resolve) => { + const stop = watch( + () => currentUser.isLoading.value, + (loading) => { + if (!loading) { + stop(); + resolve(); + } + }, + { immediate: true }, + ); + }); + } + return loadedPromise; + } + + const store: AuthStore = { + user, + isAuthenticated: computed(() => !!user.value), + isLoading: computed(() => currentUser.isLoading.value), + requires2FA, + login: async (data) => { + const response = await loginMutation.mutateAsync(data); + requires2FA.value = !!response.requires2FA; + return response; + }, + register: (data) => registerMutation.mutateAsync(data), + logout: (sessionId) => logoutMutation.mutateAsync(sessionId), + verifyEmail: (token) => verifyEmailMutation.mutateAsync(token), + refreshUser: () => currentUser.refetch(), + isLoggingIn: computed(() => loginMutation.isPending.value), + isRegistering: computed(() => registerMutation.isPending.value), + isLoggingOut: computed(() => logoutMutation.isPending.value), + isVerifyingEmail: computed(() => verifyEmailMutation.isPending.value), + whenLoaded, + }; + + // Once a user loads, the 2FA interstitial is no longer pending + watch(user, (u) => { + if (u) requires2FA.value = false; + }); + + fallback = store; + resolveStore(store); + provide(AuthKey, store); + return store; +} + +export function useAuth(): AuthStore { + const store = inject(AuthKey, null) ?? fallback; + if (!store) { + throw new Error('useAuth must be used within App (provideAuth)'); + } + return store; +} + +/** For the router guard, which runs outside any component instance. */ +export async function authStoreReady(): Promise { + return fallback ?? storeReady; +} diff --git a/apps/web-vue/src/composables/use-cart-drawer.ts b/apps/web-vue/src/composables/use-cart-drawer.ts new file mode 100644 index 0000000..535bf5e --- /dev/null +++ b/apps/web-vue/src/composables/use-cart-drawer.ts @@ -0,0 +1,20 @@ +import { ref } from 'vue'; + +// Module singleton (port of apps/web cart-drawer.context.tsx) — a boolean flag needs no +// provide/inject; every importer shares one ref, which IS the fallback pattern. +const isOpen = ref(false); + +export function useCartDrawer() { + return { + isOpen, + openDrawer: () => { + isOpen.value = true; + }, + closeDrawer: () => { + isOpen.value = false; + }, + toggleDrawer: () => { + isOpen.value = !isOpen.value; + }, + }; +} diff --git a/apps/web-vue/src/data/footer.ts b/apps/web-vue/src/data/footer.ts new file mode 100644 index 0000000..3aa9fd6 --- /dev/null +++ b/apps/web-vue/src/data/footer.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export type FooterLink = { + label: string; + href: string; + target?: '_blank'; + rel?: string; +}; + +export type FooterSection = { + title: string; + links: FooterLink[]; +}; + +export type ContactInfo = { + label: string; + content: string; + href?: string; + target?: '_blank'; + rel?: string; +}; + +export const footerSections: FooterSection[] = [ + { + title: 'SẢN PHẨM', + links: [ + { label: 'Áo thun', href: '/shop/ao-thun' }, + { label: 'Móc khóa', href: '/shop/moc-khoa' }, + { label: 'Dây đeo', href: '/shop/day-deo' }, + { label: 'Pad chuột', href: '/shop/pad-chuot' }, + ], + }, + { + title: 'VỀ CLB', + links: [ + { label: 'Về chúng tôi', href: '/about' }, + { label: 'Thành tích', href: '/achievements' }, + { label: 'Hoạt động', href: '/activities' }, + { label: 'Đối tác', href: '/partner' }, + { label: 'FAQ', href: '/faq' }, + { label: 'Thế hệ', href: '/generation' }, + ], + }, + { + title: 'ĐIỀU KHOẢN', + links: [ + { label: 'Điều khoản sử dụng', href: '/terms' }, + { label: 'Chính sách quyền riêng tư', href: '/privacy' }, + // { label: 'Chính sách đổi trả', href: '/refund' }, + ], + }, +]; + +export const contactInfo: ContactInfo[] = [ + { + label: 'Email:', + content: 'webdevstudios.org@gmail.com', + href: 'mailto:webdevstudios.org@gmail.com', + target: '_blank', + rel: 'noopener noreferrer', + }, + { + label: 'Chủ nhiệm:', + content: 'Lâm Chí Dĩnh - 0794161275', + href: 'tel:0794161275', + }, + { + label: 'Fanpage:', + content: 'facebook.com/webdevstudios.org', + href: 'https://facebook.com/webdevstudios.org', + target: '_blank', + rel: 'noopener noreferrer', + }, + { + label: 'Văn phòng:', + content: 'B8.04, tòa B, trường Đại học Công nghệ Thông tin – ĐHQG TP.HCM.', + }, +]; diff --git a/apps/web-vue/src/lib/api-client.ts b/apps/web-vue/src/lib/api-client.ts new file mode 100644 index 0000000..a883fcd --- /dev/null +++ b/apps/web-vue/src/lib/api-client.ts @@ -0,0 +1,290 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import axios, { type AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'; + +import { API_URL } from '@/lib/constants'; +import { clearCsrfToken, getCsrfToken } from '@/lib/csrf'; +import type { RefreshTokenResponse } from '@/types/auth.types'; + +// Track if we're currently refreshing to avoid multiple refresh calls +let isRefreshing = false; +let failedQueue: Array<{ + resolve: (value?: unknown) => void; + reject: (reason?: unknown) => void; +}> = []; + +const processQueue = (error: unknown, token: string | null = null) => { + failedQueue.forEach((prom) => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + + failedQueue = []; +}; + +// Create axios instance +export const apiClient: AxiosInstance = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, // Send cookies with requests +}); + +// Request interceptor - add CSRF token for state-changing requests +apiClient.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + // Skip CSRF token for safe methods (GET, HEAD, OPTIONS) + const safeMethods = ['GET', 'HEAD', 'OPTIONS']; + if (config.method && safeMethods.includes(config.method.toUpperCase())) { + return config; + } + + // Skip CSRF token for OAuth and webhook endpoints + const skipCsrfPaths = ['/auth/oauth', '/payments/webhook', '/csrf-token']; + if (config.url && skipCsrfPaths.some((path) => config.url?.includes(path))) { + return config; + } + + // Fetch and add CSRF token for state-changing requests + try { + const token = await getCsrfToken(); + if (token) { + config.headers['X-CSRF-Token'] = token; + } + } catch (error) { + // If CSRF token fetch fails, still proceed with request + // Backend will return 403 if CSRF validation fails + console.warn('Failed to fetch CSRF token:', error); + } + + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +// Response interceptor - handle errors and refresh token +apiClient.interceptors.response.use( + (response) => { + return response; + }, + async (error: AxiosError<{ data: { message?: string; error?: string } }>) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { + _retry?: boolean; + }; + + // Handle network errors + if (!error.response) { + return Promise.reject({ + message: 'Không thể kết nối đến server. Vui lòng thử lại sau.', + status: 0, + }); + } + + const status = error.response.status; + const data = error.response.data.data; + + // Handle 401 Unauthorized - try to refresh token + // Skip refresh logic for auth endpoints (login, register, etc.) and refresh endpoint + const isAuthEndpoint = + originalRequest.url?.includes('/auth/login') || + originalRequest.url?.includes('/auth/register') || + originalRequest.url?.includes('/auth/signup') || + originalRequest.url?.includes('/auth/verify-email') || + originalRequest.url?.includes('/auth/password/reset-request') || + originalRequest.url?.includes('/auth/password/reset') || + originalRequest.url === '/auth/login' || + originalRequest.url === '/auth/register' || + originalRequest.url === '/auth/signup'; + + const isRefreshEndpoint = + originalRequest.url?.includes('/auth/refresh') || originalRequest.url === '/auth/refresh'; + + // Don't refresh token for auth endpoints or refresh endpoint + // Auth endpoints return 401 when credentials are wrong, not when token expired + if (status === 401 && !originalRequest._retry && !isAuthEndpoint && !isRefreshEndpoint) { + if (isRefreshing) { + // If already refreshing, queue this request + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }) + .then(() => { + return apiClient(originalRequest); + }) + .catch((err) => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + // Try to refresh token (backend will use cookie if available) + // Use direct axios call to avoid interceptor loop + const refreshResponse = await axios.post<{ + data: RefreshTokenResponse; + }>( + `${API_URL}/auth/refresh`, + {}, + { + withCredentials: true, + }, + ); + + const { accessToken } = refreshResponse.data.data; + + // Process queued requests + processQueue(null, accessToken); + + // Retry original request + return apiClient(originalRequest); + } catch (refreshError) { + // Refresh failed - clear queue + processQueue(refreshError, null); + + // Only redirect if we're in the browser and not on public pages + if (typeof window !== 'undefined') { + const currentPath = window.location.pathname; + + // List of public routes that don't require authentication + const publicRoutes = [ + '/', // Home page + '/shop', // Shop listing + '/about', // About page + '/faq', // FAQ page + '/generation', // Generation page + '/calendar', // Calendar page + '/achievements', // Achievements page + '/activities', // Activities page + '/partner', // Partner page + '/blog', // Blog listing page + ]; + + // Check if current path is a public route or auth route + const isPublicRoute = + publicRoutes.includes(currentPath) || + currentPath.startsWith('/shop/') || // Shop product pages + currentPath.startsWith('/auth/') || // Auth pages + currentPath.startsWith('/legal/') || // Legal pages + currentPath.startsWith('/blog/'); // Blog pages + + if (!isPublicRoute) { + // Clear any auth-related state + window.location.href = '/auth/login'; + } + } + + return Promise.reject({ + message: 'Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.', + status: 401, + }); + } finally { + isRefreshing = false; + } + } + + // If refresh endpoint returns 401, don't retry - just reject + if (status === 401 && isRefreshEndpoint) { + return Promise.reject({ + message: 'Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.', + status: 401, + }); + } + + // Handle 403 Forbidden - might be CSRF token issue + if (status === 403 && !originalRequest._retry) { + const isCsrfEndpoint = + originalRequest.url?.includes('/csrf-token') || originalRequest.url === '/csrf-token'; + + // If not CSRF endpoint, try to refresh CSRF token and retry + if (!isCsrfEndpoint) { + originalRequest._retry = true; + clearCsrfToken(); // Clear invalid token + + try { + // Fetch new CSRF token + const newToken = await getCsrfToken(); + if (newToken && originalRequest.headers) { + originalRequest.headers['X-CSRF-Token'] = newToken; + } + + // Retry original request with new token + return apiClient(originalRequest); + } catch (csrfError) { + // If CSRF token fetch fails, return original error + console.error('Failed to refresh CSRF token:', csrfError); + } + } + } + + // Extract error message + let message: string; + + // Map status codes to Vietnamese messages + switch (status) { + case 400: + message = data?.message || 'Dữ liệu không hợp lệ'; + break; + case 401: + message = data?.message || 'Xác thực thất bại. Vui lòng đăng nhập lại.'; + break; + case 403: + // Check if it's a CSRF error + if ( + data?.message?.toLowerCase().includes('csrf') || + data?.error?.toLowerCase().includes('csrf') + ) { + message = 'Phiên làm việc đã hết hạn. Vui lòng thử lại.'; + } else { + message = 'Bạn không có quyền thực hiện hành động này'; + } + break; + case 404: + message = data?.message || 'Không tìm thấy tài nguyên'; + break; + case 409: + message = data?.message || 'Dữ liệu đã tồn tại'; + break; + case 422: + message = data?.message || 'Dữ liệu không hợp lệ'; + break; + case 500: + message = 'Lỗi server. Vui lòng thử lại sau.'; + break; + default: + message = data?.message || `Lỗi ${status}: ${error.message}`; + } + + return Promise.reject({ + message, + status, + data: error.response.data, + }); + }, +); diff --git a/apps/web-vue/src/lib/api/admin.ts b/apps/web-vue/src/lib/api/admin.ts new file mode 100644 index 0000000..75c1737 --- /dev/null +++ b/apps/web-vue/src/lib/api/admin.ts @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '@/lib/api-client'; + +import type { Order, OrderStatus } from './orders'; +import type { Product } from './products'; +import type { UserRole } from './users'; + +export interface AdminUser { + id: string; + email: string; + fullName: string | null; + phone: string | null; + avatar: string | null; + role: UserRole; + emailVerified: boolean; + phoneVerified: boolean; + mfaEnabled: boolean; + createdAt: string; + updatedAt: string; +} + +export interface UserListResponse { + users: AdminUser[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export interface UpdateUserRequest { + fullName?: string; + phone?: string; + avatar?: string; + role?: UserRole; +} + +export interface ProductListResponse { + products: Product[]; + total: number; +} + +export interface UpdateProductRequest { + name?: string; + description?: string; + priceCurrent?: number; + priceOriginal?: number | null; + badge?: string | null; + isPublished?: boolean; +} + +export interface OrderListResponse { + orders: Order[]; + total: number; +} + +export type PaymentTransactionStatus = 'PENDING' | 'PAID' | 'CANCELLED' | 'EXPIRED' | 'FAILED'; + +export interface PaymentTransaction { + id: string; + orderId: string; + transactionCode: string; + amount: number; + status: PaymentTransactionStatus; + paymentUrl: string | null; + createdAt: string; + updatedAt: string; +} + +export interface TransactionListResponse { + transactions: PaymentTransaction[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +export const adminApi = { + /** + * List users with pagination and role filter (Admin only) + */ + async listUsers( + page: number = 1, + limit: number = 10, + role?: UserRole, + ): Promise { + const params: Record = { + page: page.toString(), + limit: limit.toString(), + }; + if (role) { + params.role = role; + } + const response = await apiClient.get<{ data: UserListResponse }>('/users', { + params, + }); + return response.data.data; + }, + + /** + * Get user by ID (Admin only) + */ + async getUserById(id: string): Promise { + const response = await apiClient.get<{ data: AdminUser }>(`/users/${id}`); + return response.data.data; + }, + + /** + * Update user (Admin only) + */ + async updateUser(id: string, data: UpdateUserRequest): Promise { + const response = await apiClient.patch<{ data: AdminUser }>(`/users/${id}`, data); + return response.data.data; + }, + + /** + * Delete user (Admin only) + */ + async deleteUser(id: string): Promise { + await apiClient.delete(`/users/${id}`); + }, + + /** + * List all products (Admin view - includes unpublished) + */ + async listProducts(): Promise { + const response = await apiClient.get<{ data: ProductListResponse }>('/products'); + return response.data.data; + }, + + /** + * Update product (Admin only) + */ + async updateProduct(id: string, data: UpdateProductRequest): Promise { + const response = await apiClient.patch<{ data: Product }>(`/products/${id}`, data); + return response.data.data; + }, + + /** + * List orders with filters (Admin only) + */ + async listOrders( + page: number = 1, + limit: number = 10, + status?: OrderStatus, + ): Promise { + const params: Record = { + page: page.toString(), + limit: limit.toString(), + }; + if (status) { + params.status = status; + } + const response = await apiClient.get<{ data: OrderListResponse }>('/orders', { + params, + }); + return response.data.data; + }, + + /** + * List payment transactions (Admin only) + */ + async listTransactions( + page: number = 1, + limit: number = 10, + status?: PaymentTransactionStatus, + ): Promise { + const params: Record = { + page: page.toString(), + limit: limit.toString(), + }; + if (status) { + params.status = status; + } + const response = await apiClient.get<{ data: TransactionListResponse }>( + '/payments/transactions', + { + params, + }, + ); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/auth.ts b/apps/web-vue/src/lib/api/auth.ts new file mode 100644 index 0000000..b43f711 --- /dev/null +++ b/apps/web-vue/src/lib/api/auth.ts @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '@/lib/api-client'; +import type { + LoginRequest, + LoginResponse, + RefreshTokenResponse, + RegisterRequest, + RegisterResponse, + RequestPasswordResetResponse, + ResetPasswordResponse, + Session, + User, + Verify2FAResponse, + VerifyEmailResponse, +} from '@/types/auth.types'; + +export const authApi = { + /** + * Register a new user + */ + async register(data: RegisterRequest): Promise { + const response = await apiClient.post<{ data: RegisterResponse }>('/auth/register', data); + return response.data.data; + }, + + /** + * Login user + */ + async login(data: LoginRequest): Promise { + const response = await apiClient.post<{ data: LoginResponse }>('/auth/login', data); + return response.data.data; + }, + + /** + * Get current user + */ + async getCurrentUser(): Promise { + const response = await apiClient.get<{ data: User }>('/auth/me'); + return response.data.data; + }, + + /** + * Logout user + */ + async logout(sessionId?: string): Promise { + await apiClient.post('/auth/logout', { sessionId }); + }, + + /** + * Verify email with token + */ + async verifyEmail(token: string): Promise { + const response = await apiClient.get<{ data: VerifyEmailResponse }>('/auth/verify-email', { + params: { token }, + }); + return response.data.data; + }, + + /** + * Refresh access token + */ + async refreshToken(refreshToken?: string): Promise { + const response = await apiClient.post<{ data: RefreshTokenResponse }>( + '/auth/refresh', + refreshToken ? { refreshToken } : {}, + ); + return response.data.data; + }, + + /** + * Request password reset + */ + async requestPasswordReset(email: string): Promise { + const response = await apiClient.post<{ + data: RequestPasswordResetResponse; + }>('/auth/password/reset-request', { email }); + return response.data.data; + }, + + /** + * Reset password with token + */ + async resetPassword(token: string, newPassword: string): Promise { + const response = await apiClient.post<{ data: ResetPasswordResponse }>('/auth/password/reset', { + token, + newPassword, + }); + return response.data.data; + }, + + /** + * Verify 2FA code + */ + async verify2FA(code: string, sessionId?: string): Promise { + const response = await apiClient.post<{ data: Verify2FAResponse }>('/auth/2fa/verify', { + code, + sessionId, + }); + return response.data.data; + }, + + /** + * Get user sessions + */ + async getSessions(): Promise { + const response = await apiClient.get<{ data: Session[] }>('/auth/sessions'); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/blog.ts b/apps/web-vue/src/lib/api/blog.ts new file mode 100644 index 0000000..0b382a5 --- /dev/null +++ b/apps/web-vue/src/lib/api/blog.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; + +export interface BlogPostAuthor { + id: string; + fullName: string | null; + avatar: string | null; +} + +export interface BlogPost { + id: string; + slug: string; + title: string; + contentUrl: string; + contentSize: number | null; + excerpt: string | null; + coverImage: string | null; + author: BlogPostAuthor; + isPublished: boolean; + publishedAt: string | null; + viewCount: number; + metaTitle: string | null; + metaDescription: string | null; + createdAt: string; + updatedAt: string; +} + +export interface BlogPostWithContent extends BlogPost { + content: string; +} + +export interface BlogPostListResponse { + posts: BlogPost[]; + total: number; + page: number; + pageSize: number; +} + +export interface CreateBlogPostDto { + slug: string; + title: string; + content: string; + excerpt?: string | null; + coverImage?: string | null; + isPublished?: boolean; + metaTitle?: string | null; + metaDescription?: string | null; +} + +export interface UpdateBlogPostDto { + title?: string; + content?: string; + excerpt?: string | null; + coverImage?: string | null; + isPublished?: boolean; + metaTitle?: string | null; + metaDescription?: string | null; +} + +export const blogApi = { + /** + * List blog posts + */ + async listPosts(options?: { page?: number; pageSize?: number }): Promise { + const response = await apiClient.get<{ data: BlogPostListResponse }>('/blog/posts', { + params: options, + }); + return response.data.data; + }, + + /** + * List all blog posts (Admin only - includes unpublished) + */ + async listAllPosts(options?: { + page?: number; + pageSize?: number; + }): Promise { + const response = await apiClient.get<{ data: BlogPostListResponse }>('/blog/posts/admin/all', { + params: options, + }); + return response.data.data; + }, + + /** + * Search blog posts + */ + async searchPosts( + query: string, + options?: { + page?: number; + pageSize?: number; + }, + ): Promise { + const response = await apiClient.get<{ data: BlogPostListResponse }>('/blog/posts/search', { + params: { + q: query, + ...options, + }, + }); + return response.data.data; + }, + + /** + * Get blog post by slug + */ + async getPostBySlug( + slug: string, + includeContent = false, + ): Promise { + const response = await apiClient.get<{ + data: BlogPost | BlogPostWithContent; + }>(`/blog/posts/${slug}`, { + params: { + includeContent, + }, + }); + return response.data.data; + }, + + /** + * Get blog post by ID (Admin only) + */ + async getPostById(id: string, includeContent = true): Promise { + const response = await apiClient.get<{ + data: BlogPost | BlogPostWithContent; + }>(`/blog/posts/admin/${id}`, { + params: { + includeContent, + }, + }); + return response.data.data; + }, + + /** + * Create blog post (Admin only) + */ + async createPost(dto: CreateBlogPostDto): Promise { + const response = await apiClient.post<{ data: BlogPost }>('/blog/posts', dto); + return response.data.data; + }, + + /** + * Update blog post (Admin only) + */ + async updatePost(id: string, dto: UpdateBlogPostDto): Promise { + const response = await apiClient.patch<{ data: BlogPost }>(`/blog/posts/${id}`, dto); + return response.data.data; + }, + + /** + * Delete blog post (Admin only) + */ + async deletePost(id: string): Promise { + await apiClient.delete(`/blog/posts/${id}`); + }, + + /** + * Publish blog post (Admin only) + */ + async publishPost(id: string): Promise { + const response = await apiClient.post<{ data: BlogPost }>(`/blog/posts/${id}/publish`); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/cart.ts b/apps/web-vue/src/lib/api/cart.ts new file mode 100644 index 0000000..6a69540 --- /dev/null +++ b/apps/web-vue/src/lib/api/cart.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; +import type { ProductSize } from './products'; + +export interface CartItem { + id: string; + productId: string; + productName: string; + productSlug: string; + productPrice: number; + productImage: string; + size: ProductSize | null; + quantity: number; + subtotal: number; + stockAvailable: number; +} + +export interface Cart { + id: string; + items: CartItem[]; + totalItems: number; + totalAmount: number; + updatedAt: string; +} + +export interface AddToCartRequest { + productId: string; + size?: ProductSize; + quantity: number; +} + +export interface UpdateCartItemRequest { + quantity: number; +} + +export const cartApi = { + /** + * Get user's cart + */ + async getCart(): Promise { + const response = await apiClient.get<{ data: Cart }>('/cart'); + return response.data.data; + }, + + /** + * Add item to cart + */ + async addToCart(data: AddToCartRequest): Promise { + const response = await apiClient.post<{ data: Cart }>('/cart/items', data); + return response.data.data; + }, + + /** + * Update cart item quantity + */ + async updateCartItem(cartItemId: string, data: UpdateCartItemRequest): Promise { + const response = await apiClient.patch<{ data: Cart }>(`/cart/items/${cartItemId}`, data); + return response.data.data; + }, + + /** + * Remove item from cart + */ + async removeFromCart(cartItemId: string): Promise { + const response = await apiClient.delete<{ data: Cart }>(`/cart/items/${cartItemId}`); + return response.data.data; + }, + + /** + * Clear cart + */ + async clearCart(): Promise { + const response = await apiClient.delete<{ data: Cart }>('/cart'); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/events.ts b/apps/web-vue/src/lib/api/events.ts new file mode 100644 index 0000000..ed69b65 --- /dev/null +++ b/apps/web-vue/src/lib/api/events.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository on GitHub + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { EventType } from '@/lib/events/types'; + +import { apiClient } from '../api-client'; + +export interface ApiEvent { + id: string; + title: string; + description?: string; + startDate: string; + endDate: string; + location?: string; + type: EventType; + organizer?: string; + attendees?: number; + surveyLink?: string; + createdAt: string; + updatedAt: string; +} + +export const eventsApi = { + /** + * Get all events + */ + async getEvents(): Promise { + const response = await apiClient.get<{ data: ApiEvent[] }>('/events'); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/hooks/use-auth.ts b/apps/web-vue/src/lib/api/hooks/use-auth.ts new file mode 100644 index 0000000..0249668 --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-auth.ts @@ -0,0 +1,356 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; +import { onScopeDispose, ref } from 'vue'; +import { useRouter } from 'vue-router'; + +import { authApi } from '@/lib/api/auth'; +import { API_URL, SITE_URL } from '@/lib/constants'; +import { clearCsrfToken } from '@/lib/csrf'; +import { toast } from '@/lib/toast'; +import type { + LoginRequest, + LoginResponse, + OAuthProvider, + RegisterRequest, +} from '@/types/auth.types'; + +// Query Keys +export const authKeys = { + all: ['auth'] as const, + currentUser: () => [...authKeys.all, 'currentUser'] as const, +}; + +// Shared options so the router guard can await the same cached query +export function currentUserQueryOptions() { + return { + queryKey: authKeys.currentUser(), + queryFn: () => authApi.getCurrentUser(), + staleTime: 5 * 60 * 1000, // 5 minutes + retry: false, + // Don't refetch on window focus if we're on auth pages + refetchOnWindowFocus: false, + // Don't refetch on mount if we're on auth pages + refetchOnMount: () => { + if (typeof window !== 'undefined') { + const path = window.location.pathname; + // Skip refetch if on auth pages + if (path.startsWith('/auth/')) { + return false; + } + } + return true; + }, + }; +} + +// Query: Get current user +export function useCurrentUser() { + return useQuery(currentUserQueryOptions()); +} + +// Helper function to get redirect URL from query params +function getRedirectUrlFromQuery(): string | null { + if (typeof window === 'undefined') { + return null; + } + + const searchParams = new URLSearchParams(window.location.search); + const redirectUrl = searchParams.get('redirect_url'); + if (!redirectUrl) { + return null; + } + + // Validate redirect URL - only allow internal URLs + try { + const url = new URL(redirectUrl, SITE_URL); + const frontendOrigin = new URL(SITE_URL).origin; + + // Only allow same origin redirects + if (url.origin !== frontendOrigin) { + return null; + } + + return url.pathname + url.search; + } catch { + // Invalid URL, return null + return null; + } +} + +// Mutation: Login +export function useLogin() { + const queryClient = useQueryClient(); + const router = useRouter(); + + return useMutation({ + mutationFn: (data: LoginRequest) => authApi.login(data), + onSuccess: async (response: LoginResponse) => { + // Clear old CSRF token and fetch new one for new session + clearCsrfToken(); + + // Check if 2FA is required + if (response.requires2FA) { + toast.info('Vui lòng xác thực 2FA để tiếp tục'); + router.push('/auth/2fa'); + return; + } + + // Invalidate and refetch current user + await queryClient.invalidateQueries({ queryKey: authKeys.currentUser() }); + toast.success('Đăng nhập thành công!'); + + // Get redirect URL from query params only after successful login + const redirectUrl = getRedirectUrlFromQuery(); + // Redirect to redirectUrl if provided, otherwise redirect to homepage + router.push(redirectUrl || '/'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Đăng nhập thất bại. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Register +export function useRegister() { + const router = useRouter(); + + return useMutation({ + mutationFn: (data: RegisterRequest) => authApi.register(data), + onSuccess: () => { + toast.success('Đăng ký thành công! Vui lòng kiểm tra email để xác thực tài khoản.'); + + // Get redirect URL from query params only after successful register + const redirectUrl = getRedirectUrlFromQuery(); + // Register always redirects to login for email verification, unless redirect_url is provided + const finalRedirectUrl = redirectUrl || '/auth/login'; + router.push(finalRedirectUrl); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Đăng ký thất bại. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Logout +export function useLogout() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (sessionId?: string) => authApi.logout(sessionId), + onSuccess: () => { + clearCsrfToken(); + queryClient.clear(); + toast.success('Đã đăng xuất'); + window.location.reload(); + }, + onError: (error: unknown) => { + // Still clear CSRF token and queries even on error + clearCsrfToken(); + queryClient.clear(); + const errorMessage = error instanceof Error ? error.message : 'Đăng xuất thất bại'; + toast.error(errorMessage); + window.location.reload(); + }, + }); +} + +// Mutation: Verify Email +export function useVerifyEmail() { + const router = useRouter(); + + return useMutation({ + mutationFn: (token: string) => authApi.verifyEmail(token), + onSuccess: () => { + toast.success('Email đã được xác thực thành công!'); + setTimeout(() => { + router.push('/auth/login'); + }, 2000); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Token không hợp lệ hoặc đã hết hạn'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Request Password Reset +export function useRequestPasswordReset() { + return useMutation({ + mutationFn: (email: string) => authApi.requestPasswordReset(email), + onSuccess: () => { + toast.success( + 'Vui lòng kiểm tra email để nhận link reset mật khẩu. Nếu không thấy email, vui lòng kiểm tra thư mục spam.', + ); + }, + onError: (error: unknown) => { + // Backend always returns success for security, but handle errors just in case + const errorMessage = + error instanceof Error + ? error.message + : 'Không thể gửi email reset mật khẩu. Vui lòng thử lại sau.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Reset Password +export function useResetPassword() { + const router = useRouter(); + + return useMutation({ + mutationFn: (data: { token: string; newPassword: string }) => + authApi.resetPassword(data.token, data.newPassword), + onSuccess: () => { + toast.success('Đặt lại mật khẩu thành công!'); + setTimeout(() => { + router.push('/auth/login'); + }, 2000); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error + ? error.message + : 'Token không hợp lệ hoặc đã hết hạn. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Verify 2FA +export function useVerify2FA() { + const queryClient = useQueryClient(); + const router = useRouter(); + + return useMutation({ + mutationFn: (data: { code: string; sessionId?: string }) => + authApi.verify2FA(data.code, data.sessionId), + onSuccess: async (response) => { + // If login flow (has tokens), invalidate queries and redirect + if (response.accessToken && response.refreshToken) { + await queryClient.invalidateQueries({ + queryKey: authKeys.currentUser(), + }); + toast.success('Xác thực 2FA thành công!'); + router.push('/'); + } else { + // Setup flow (just verified) + toast.success('2FA đã được kích hoạt thành công!'); + } + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Mã xác thực không hợp lệ. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Hook: OAuth popup flow +export function useOAuth() { + const queryClient = useQueryClient(); + const isLoading = ref(false); + let popup: Window | null = null; + let messageHandler: ((event: MessageEvent) => void) | null = null; + let checkClosed: ReturnType | null = null; + + const initiateOAuth = (provider: OAuthProvider) => { + if (isLoading.value) { + return; + } + + isLoading.value = true; + + // Get redirect URL from query params + const redirectUrl = getRedirectUrlFromQuery(); + const oauthUrl = redirectUrl + ? `${API_URL}/auth/oauth/${provider}?${new URLSearchParams({ redirect_url: redirectUrl }).toString()}` + : `${API_URL}/auth/oauth/${provider}`; + + // Calculate popup position (center of screen) + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + // Open popup + const opened = window.open( + oauthUrl, + 'oauth-popup', + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`, + ); + + if (!opened) { + toast.error('Không thể mở popup. Vui lòng cho phép popup trong trình duyệt.'); + isLoading.value = false; + return; + } + + popup = opened; + + // Listen for messages from popup + messageHandler = (event: MessageEvent) => { + // Verify origin for security + const allowedOrigin = new URL(SITE_URL).origin; + if (event.origin !== allowedOrigin) { + return; + } + + if (event.data?.type === 'oauth-success') { + popup?.close(); + popup = null; + + const redirectUrl = event.data.data?.redirectUrl || '/'; + + queryClient.invalidateQueries({ queryKey: authKeys.currentUser() }); + toast.success('Đăng nhập thành công!'); + + sessionStorage.setItem('oauth_redirect_url', redirectUrl); + window.location.reload(); + } else if (event.data?.type === 'oauth-error') { + popup?.close(); + popup = null; + + const errorMessage = + event.data.data?.errorDescription || + event.data.data?.error || + 'Đăng nhập thất bại. Vui lòng thử lại.'; + toast.error(errorMessage); + isLoading.value = false; + } + }; + window.addEventListener('message', messageHandler); + + // Check if popup was closed manually + checkClosed = setInterval(() => { + if (popup?.closed) { + if (checkClosed) clearInterval(checkClosed); + checkClosed = null; + isLoading.value = false; + cleanup(); + } + }, 500); + }; + + function cleanup() { + if (popup && !popup.closed) { + popup.close(); + } + if (messageHandler) { + window.removeEventListener('message', messageHandler); + messageHandler = null; + } + popup = null; + } + + // Cleanup on unmount + onScopeDispose(cleanup); + + return { + initiateOAuth, + isLoading, + }; +} diff --git a/apps/web-vue/src/lib/api/hooks/use-cart.ts b/apps/web-vue/src/lib/api/hooks/use-cart.ts new file mode 100644 index 0000000..f6667cd --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-cart.ts @@ -0,0 +1,230 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; + +import { useCartDrawer } from '@/composables/use-cart-drawer'; +import { + type AddToCartRequest, + type Cart, + cartApi, + type UpdateCartItemRequest, +} from '@/lib/api/cart'; +import { useDebouncedCallback } from '@/lib/hooks/use-debounce'; +import { toast } from '@/lib/toast'; + +// Query Keys +export const cartKeys = { + all: ['cart'] as const, + current: () => [...cartKeys.all, 'current'] as const, +}; + +// Query: Get user's cart +export function useCart() { + return useQuery({ + queryKey: cartKeys.current(), + queryFn: () => cartApi.getCart(), + staleTime: 30 * 1000, // 30 seconds + retry: false, + }); +} + +// Mutation: Add to cart +export function useAddToCart() { + const queryClient = useQueryClient(); + const { openDrawer } = useCartDrawer(); + + return useMutation({ + mutationFn: (data: AddToCartRequest) => cartApi.addToCart(data), + onSuccess: () => { + // Invalidate cart to refetch + queryClient.invalidateQueries({ queryKey: cartKeys.current() }); + toast.success('Đã thêm vào giỏ hàng!'); + openDrawer(); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể thêm vào giỏ hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Helper function to update cart item quantity in cache +function updateCartItemQuantity( + cart: Cart | undefined, + cartItemId: string, + quantity: number, +): Cart | undefined { + if (!cart) return cart; + + const updatedItems = cart.items.map((item) => { + if (item.id === cartItemId) { + const newSubtotal = item.productPrice * quantity; + return { + ...item, + quantity, + subtotal: newSubtotal, + }; + } + return item; + }); + + const totalAmount = updatedItems.reduce((sum, item) => sum + item.subtotal, 0); + const totalItems = updatedItems.reduce((sum, item) => sum + item.quantity, 0); + + return { + ...cart, + items: updatedItems, + totalAmount, + totalItems, + }; +} + +// Mutation: Update cart item with optimistic updates and debounce +export function useUpdateCartItem() { + const queryClient = useQueryClient(); + + // Track original cart state before optimistic updates for rollback + const originalCart = new Map(); + + const mutation = useMutation({ + mutationFn: ({ cartItemId, data }: { cartItemId: string; data: UpdateCartItemRequest }) => + cartApi.updateCartItem(cartItemId, data), + onMutate: async ({ cartItemId }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: cartKeys.current() }); + + // Get the original cart state (stored when optimistic update was applied) + const previousCart = originalCart.get(cartItemId); + + // Return context with original snapshot + return { previousCart }; + }, + onSuccess: (data, variables) => { + // Remove from tracking + originalCart.delete(variables.cartItemId); + // Update cache with server response + queryClient.setQueryData(cartKeys.current(), data); + }, + onError: (error: unknown, variables, context) => { + // Remove from tracking + originalCart.delete(variables.cartItemId); + // Rollback to original value on error + if (context?.previousCart !== undefined) { + queryClient.setQueryData(cartKeys.current(), context.previousCart); + } + + const errorMessage = + error instanceof Error ? error.message : 'Không thể cập nhật giỏ hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); + + // Debounced API call + const debouncedApiCall = useDebouncedCallback( + (variables: { cartItemId: string; data: UpdateCartItemRequest }) => { + mutation.mutate(variables); + }, + 500, + ); + + // Combined mutate function: optimistic update immediately + debounced API call + const mutate = (variables: { cartItemId: string; data: UpdateCartItemRequest }) => { + // Get current cart state (may already be optimistic from previous clicks) + const currentCart = queryClient.getQueryData(cartKeys.current()); + + if (currentCart) { + // Store current state as "previous" for rollback (always update to latest) + originalCart.set(variables.cartItemId, currentCart); + + // Apply optimistic update immediately + const optimisticCart = updateCartItemQuantity( + currentCart, + variables.cartItemId, + variables.data.quantity, + ); + queryClient.setQueryData(cartKeys.current(), optimisticCart); + } + + // Debounce the API call + debouncedApiCall(variables); + }; + + return { + ...mutation, + mutate, + }; +} + +// Helper function to remove cart item from cache +function removeCartItem(cart: Cart | undefined, cartItemId: string): Cart | undefined { + if (!cart) return cart; + + const updatedItems = cart.items.filter((item) => item.id !== cartItemId); + + const totalAmount = updatedItems.reduce((sum, item) => sum + item.subtotal, 0); + const totalItems = updatedItems.reduce((sum, item) => sum + item.quantity, 0); + + return { + ...cart, + items: updatedItems, + totalAmount, + totalItems, + }; +} + +// Mutation: Remove from cart with optimistic updates +export function useRemoveFromCart() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (cartItemId: string) => cartApi.removeFromCart(cartItemId), + onMutate: async (cartItemId) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: cartKeys.current() }); + + // Snapshot previous value + const previousCart = queryClient.getQueryData(cartKeys.current()); + + // Optimistically remove item from cache + if (previousCart) { + const optimisticCart = removeCartItem(previousCart, cartItemId); + queryClient.setQueryData(cartKeys.current(), optimisticCart); + } + + // Return context with snapshot + return { previousCart }; + }, + onSuccess: (data) => { + // Update cache with server response + queryClient.setQueryData(cartKeys.current(), data); + toast.success('Đã xóa khỏi giỏ hàng'); + }, + onError: (error: unknown, _cartItemId, context) => { + // Rollback to previous value on error + if (context?.previousCart) { + queryClient.setQueryData(cartKeys.current(), context.previousCart); + } + + const errorMessage = + error instanceof Error ? error.message : 'Không thể xóa khỏi giỏ hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Clear cart +export function useClearCart() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => cartApi.clearCart(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: cartKeys.current() }); + toast.success('Đã xóa tất cả khỏi giỏ hàng'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể xóa giỏ hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-events.ts b/apps/web-vue/src/lib/api/hooks/use-events.ts new file mode 100644 index 0000000..8a9d5e7 --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-events.ts @@ -0,0 +1,45 @@ +import { useQuery } from '@tanstack/vue-query'; +import { computed } from 'vue'; + +import { eventsApi, type ApiEvent } from '@/lib/api/events'; +import { mockEvents } from '@/lib/events/mock-events'; +import { EventType, type Event } from '@/lib/events/types'; + +// ponytail: mock data doubles as the offline/failed-request fallback adapter +export const eventKeys = { + all: ['events'] as const, +}; + +const knownTypes = new Set(Object.values(EventType)); + +function toEvent(e: ApiEvent): Event { + return { + id: e.id, + title: e.title, + description: e.description, + start: new Date(e.startDate), + end: new Date(e.endDate), + location: e.location, + type: knownTypes.has(e.type) ? e.type : EventType.OTHER, + organizer: e.organizer, + attendees: e.attendees, + surveyLink: e.surveyLink, + }; +} + +// Query: list events for the calendar. Falls back to mock data while the API +// is unavailable so the calendar keeps rendering. +export function useEvents() { + const query = useQuery({ + queryKey: eventKeys.all, + queryFn: async () => (await eventsApi.getEvents()).map(toEvent), + staleTime: 5 * 60 * 1000, + }); + + return { + ...query, + events: computed(() => + query.isError.value || query.data.value === undefined ? mockEvents : query.data.value, + ), + }; +} diff --git a/apps/web-vue/src/lib/api/hooks/use-orders.ts b/apps/web-vue/src/lib/api/hooks/use-orders.ts new file mode 100644 index 0000000..0c56d06 --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-orders.ts @@ -0,0 +1,115 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import { type CreateOrderRequest, ordersApi, type OrderStatus } from '@/lib/api/orders'; +import { toast } from '@/lib/toast'; + +// Reactive-params convention for all hook composables in this directory: params accept +// MaybeRefOrGetter (plain value | ref | getter); queryKey uses computed(() => ...toValue(param)), +// queryFn reads toValue(param) at call time, `enabled` is a computed. Plain args work unchanged. +// useSuspense* hooks keep their apps/web names but are plain useQuery — Vue has no suspense +// data boundary; pages v-if on .isPending instead. + +// Query Keys +export const orderKeys = { + all: ['orders'] as const, + lists: () => [...orderKeys.all, 'list'] as const, + list: (page?: number, limit?: number) => [...orderKeys.lists(), page, limit] as const, + details: () => [...orderKeys.all, 'detail'] as const, + detail: (id: string) => [...orderKeys.details(), id] as const, +}; + +// Query: List user orders +export function useOrders( + page: MaybeRefOrGetter = 1, + limit: MaybeRefOrGetter = 10, +) { + return useQuery({ + queryKey: computed(() => orderKeys.list(toValue(page), toValue(limit))), + queryFn: () => ordersApi.listOrders(toValue(page), toValue(limit)), + staleTime: 30 * 1000, // 30 seconds + }); +} + +// Suspense Query: List user orders (see convention note — plain useQuery in Vue) +export function useSuspenseOrders( + page: MaybeRefOrGetter = 1, + limit: MaybeRefOrGetter = 10, +) { + return useOrders(page, limit); +} + +// Query: Get order by ID +export function useOrder(orderId: MaybeRefOrGetter) { + return useQuery({ + queryKey: computed(() => orderKeys.detail(toValue(orderId))), + queryFn: () => ordersApi.getOrderById(toValue(orderId)), + enabled: computed(() => !!toValue(orderId)), + staleTime: 30 * 1000, // 30 seconds + }); +} + +// Mutation: Create order +export function useCreateOrder() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateOrderRequest) => ordersApi.createOrder(data), + onSuccess: () => { + // Invalidate orders list and cart + queryClient.invalidateQueries({ queryKey: orderKeys.lists() }); + queryClient.invalidateQueries({ queryKey: ['cart'] }); + toast.success('Đơn hàng đã được tạo thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể tạo đơn hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Cancel order +export function useCancelOrder() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (orderId: string) => ordersApi.cancelOrder(orderId), + onSuccess: (_, orderId) => { + // Invalidate order detail and orders list + queryClient.invalidateQueries({ queryKey: orderKeys.detail(orderId) }); + queryClient.invalidateQueries({ queryKey: orderKeys.lists() }); + toast.success('Đơn hàng đã được hủy thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể hủy đơn hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Update order status (admin only) +export function useUpdateOrderStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ orderId, status }: { orderId: string; status: OrderStatus }) => + ordersApi.updateOrderStatus(orderId, status), + onSuccess: (_, variables) => { + // Invalidate order detail and orders list + queryClient.invalidateQueries({ + queryKey: orderKeys.detail(variables.orderId), + }); + queryClient.invalidateQueries({ queryKey: orderKeys.lists() }); + toast.success('Trạng thái đơn hàng đã được cập nhật!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error + ? error.message + : 'Không thể cập nhật trạng thái đơn hàng. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-payments.ts b/apps/web-vue/src/lib/api/hooks/use-payments.ts new file mode 100644 index 0000000..8fa55c3 --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-payments.ts @@ -0,0 +1,25 @@ +import { useMutation, useQuery } from '@tanstack/vue-query'; +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import { + createPaymentLink, + type CreatePaymentLinkRequest, + type CreatePaymentLinkResponse, + verifyPayment, +} from '../payments'; + +// Reactive-params convention: see note in use-orders.ts +export function useCreatePaymentLink() { + return useMutation({ + mutationFn: createPaymentLink, + }); +} + +export function useVerifyPayment(transactionCode: MaybeRefOrGetter) { + return useQuery({ + queryKey: computed(() => ['payment', 'verify', toValue(transactionCode)]), + queryFn: () => verifyPayment(toValue(transactionCode)!), + enabled: computed(() => !!toValue(transactionCode)), + refetchInterval: 5000, // Poll every 5 seconds + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-products.ts b/apps/web-vue/src/lib/api/hooks/use-products.ts new file mode 100644 index 0000000..c27ddab --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-products.ts @@ -0,0 +1,54 @@ +import { useQuery } from '@tanstack/vue-query'; +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import { productsApi, type ProductSize, type ProductSlug } from '@/lib/api/products'; + +// Reactive-params convention: see note in use-orders.ts + +// Query Keys +export const productKeys = { + all: ['products'] as const, + lists: () => [...productKeys.all, 'list'] as const, + list: () => [...productKeys.lists()] as const, + details: () => [...productKeys.all, 'detail'] as const, + detail: (slug: ProductSlug) => [...productKeys.details(), slug] as const, + stock: (slug: ProductSlug, size?: ProductSize) => + [...productKeys.detail(slug), 'stock', size] as const, +}; + +// Query: List all products +export function useProducts() { + return useQuery({ + queryKey: productKeys.list(), + queryFn: () => productsApi.listProducts(), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +// Query: Get product by slug +export function useProduct(slug: MaybeRefOrGetter) { + return useQuery({ + queryKey: computed(() => productKeys.detail(toValue(slug))), + queryFn: () => productsApi.getProductBySlug(toValue(slug)), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +// Suspense Query: Get product by slug (see convention note — plain useQuery in Vue) +export function useSuspenseProduct(slug: MaybeRefOrGetter) { + return useProduct(slug); +} + +// Query: Get product stock +export function useProductStock( + slug: MaybeRefOrGetter, + size?: MaybeRefOrGetter, + enabled: MaybeRefOrGetter = true, +) { + return useQuery({ + queryKey: computed(() => productKeys.stock(toValue(slug), toValue(size))), + queryFn: () => productsApi.getProductStock(toValue(slug), toValue(size)), + enabled: computed(() => toValue(enabled)), + staleTime: 30 * 1000, // 30 seconds + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-reviews.ts b/apps/web-vue/src/lib/api/hooks/use-reviews.ts new file mode 100644 index 0000000..e720f6d --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-reviews.ts @@ -0,0 +1,96 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import { type CreateReviewRequest, type ProductSlug, reviewsApi } from '@/lib/api/reviews'; +import { toast } from '@/lib/toast'; + +// Reactive-params convention: see note in use-orders.ts + +// Query Keys +export const reviewKeys = { + all: ['reviews'] as const, + product: (slug: ProductSlug) => [...reviewKeys.all, 'product', slug] as const, + productList: (slug: ProductSlug, page?: number, limit?: number) => + [...reviewKeys.product(slug), 'list', page, limit] as const, +}; + +// Query: Get product reviews +export function useProductReviews( + productSlug: MaybeRefOrGetter, + page: MaybeRefOrGetter = 1, + limit: MaybeRefOrGetter = 10, +) { + return useQuery({ + queryKey: computed(() => + reviewKeys.productList(toValue(productSlug), toValue(page), toValue(limit)), + ), + queryFn: () => + reviewsApi.getProductReviews(toValue(productSlug), toValue(page), toValue(limit)), + staleTime: 30 * 1000, // 30 seconds + }); +} + +// Mutation: Create review +export function useCreateReview(productSlug: MaybeRefOrGetter) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateReviewRequest) => reviewsApi.createReview(toValue(productSlug), data), + onSuccess: () => { + // Invalidate all reviews for this product to refetch + queryClient.invalidateQueries({ + queryKey: reviewKeys.product(toValue(productSlug)), + }); + toast.success('Đánh giá đã được gửi thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể gửi đánh giá. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Update review +export function useUpdateReview() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + reviewId, + data, + }: { + reviewId: string; + data: { rating?: number; comment?: string | null }; + }) => reviewsApi.updateReview(reviewId, data), + onSuccess: () => { + // Invalidate all reviews queries + queryClient.invalidateQueries({ queryKey: reviewKeys.all }); + toast.success('Đánh giá đã được cập nhật thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể cập nhật đánh giá. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Delete review +export function useDeleteReview() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (reviewId: string) => reviewsApi.deleteReview(reviewId), + onSuccess: () => { + // Invalidate all reviews queries + queryClient.invalidateQueries({ queryKey: reviewKeys.all }); + toast.success('Đánh giá đã được xóa thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Không thể xóa đánh giá. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-settings.ts b/apps/web-vue/src/lib/api/hooks/use-settings.ts new file mode 100644 index 0000000..f14b3de --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-settings.ts @@ -0,0 +1,41 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; + +import { authApi } from '@/lib/api/auth'; +import { toast } from '@/lib/toast'; + +// Query Keys +export const settingsKeys = { + all: ['settings'] as const, + sessions: () => [...settingsKeys.all, 'sessions'] as const, +}; + +// Query: Get user sessions +export function useSessions() { + return useQuery({ + queryKey: settingsKeys.sessions(), + queryFn: () => authApi.getSessions(), + staleTime: 1 * 60 * 1000, // 1 minute + retry: false, + }); +} + +// Mutation: Revoke session +export function useRevokeSession() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (sessionId?: string) => authApi.logout(sessionId), + onSuccess: () => { + // Invalidate sessions list + queryClient.invalidateQueries({ queryKey: settingsKeys.sessions() }); + toast.success('Đã đăng xuất phiên làm việc'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error + ? error.message + : 'Không thể đăng xuất phiên làm việc. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} diff --git a/apps/web-vue/src/lib/api/hooks/use-user.ts b/apps/web-vue/src/lib/api/hooks/use-user.ts new file mode 100644 index 0000000..c542beb --- /dev/null +++ b/apps/web-vue/src/lib/api/hooks/use-user.ts @@ -0,0 +1,63 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'; + +import { authKeys } from '@/lib/api/hooks/use-auth'; +import { type UpdateProfileRequest, usersApi } from '@/lib/api/users'; +import { toast } from '@/lib/toast'; + +// Query Keys +export const userKeys = { + all: ['users'] as const, + profile: () => [...userKeys.all, 'profile'] as const, +}; + +// Query: Get user profile +export function useUserProfile() { + return useQuery({ + queryKey: userKeys.profile(), + queryFn: () => usersApi.getUserProfile(), + staleTime: 5 * 60 * 1000, // 5 minutes + retry: false, + }); +} + +// Mutation: Update profile +export function useUpdateProfile() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: UpdateProfileRequest) => usersApi.updateProfile(data), + onSuccess: (updatedUser) => { + // Update both user profile cache and current user cache + queryClient.setQueryData(userKeys.profile(), updatedUser); + queryClient.setQueryData(authKeys.currentUser(), updatedUser); + toast.success('Cập nhật hồ sơ thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error ? error.message : 'Cập nhật hồ sơ thất bại. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} + +// Mutation: Update avatar +export function useUpdateAvatar() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (file: File) => usersApi.updateAvatar(file), + onSuccess: (updatedUser) => { + // Update both user profile cache and current user cache + queryClient.setQueryData(userKeys.profile(), updatedUser); + queryClient.setQueryData(authKeys.currentUser(), updatedUser); + toast.success('Cập nhật ảnh đại diện thành công!'); + }, + onError: (error: unknown) => { + const errorMessage = + error instanceof Error + ? error.message + : 'Cập nhật ảnh đại diện thất bại. Vui lòng thử lại.'; + toast.error(errorMessage); + }, + }); +} diff --git a/apps/web-vue/src/lib/api/orders.ts b/apps/web-vue/src/lib/api/orders.ts new file mode 100644 index 0000000..9c368b2 --- /dev/null +++ b/apps/web-vue/src/lib/api/orders.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; +import type { ProductSize } from './products'; + +export type OrderStatus = + | 'PENDING' + | 'CONFIRMED' + | 'PROCESSING' + | 'SHIPPING' + | 'DELIVERED' + | 'CANCELLED' + | 'RETURNED'; + +export type PaymentStatus = 'PENDING' | 'PAID' | 'FAILED' | 'REFUNDED'; + +export interface OrderItem { + id: string; + productId: string | null; + productSlug: string; + productName: string; + size: ProductSize | null; + price: number; + quantity: number; + subtotal: number; +} + +export interface ShippingAddress { + fullName: string; + phone: string; + addressLine1: string; + addressLine2?: string | null; + city: string; + district: string; + ward: string; + postalCode: string; +} + +export interface Order { + id: string; + code: string; + status: OrderStatus; + paymentStatus: PaymentStatus; + totalAmount: number; + shippingFee: number; + discountValue: number; + shippingAddress: ShippingAddress; + items: OrderItem[]; + createdAt: string; + updatedAt: string; +} + +export type OrderType = 'FROM_CART' | 'DIRECT_PURCHASE'; + +export interface CreateOrderRequest { + shippingAddress: ShippingAddress; + orderType?: OrderType; + productId?: string; + productSlug?: string; + size?: ProductSize; + quantity?: number; +} + +export interface OrderListResponse { + orders: Order[]; + total: number; +} + +export interface UpdateOrderStatusRequest { + status: OrderStatus; +} + +export const ordersApi = { + /** + * Create order from cart + */ + async createOrder(data: CreateOrderRequest): Promise { + const response = await apiClient.post<{ data: Order }>('/orders', data); + return response.data.data; + }, + + /** + * Get order by ID + */ + async getOrderById(orderId: string): Promise { + const response = await apiClient.get<{ data: Order }>(`/orders/${orderId}`); + return response.data.data; + }, + + /** + * List user's orders + */ + async listOrders(page?: number, limit?: number): Promise { + const params: Record = {}; + if (page) params.page = page.toString(); + if (limit) params.limit = limit.toString(); + const response = await apiClient.get<{ data: OrderListResponse }>('/orders', { + params, + }); + return response.data.data; + }, + + /** + * Cancel order + */ + async cancelOrder(orderId: string): Promise { + const response = await apiClient.patch<{ data: Order }>(`/orders/${orderId}/cancel`); + return response.data.data; + }, + + /** + * Update order status (Admin only) + */ + async updateOrderStatus(orderId: string, status: OrderStatus): Promise { + const response = await apiClient.patch<{ data: Order }>(`/orders/${orderId}/status`, { + status, + }); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/payments.ts b/apps/web-vue/src/lib/api/payments.ts new file mode 100644 index 0000000..fe15435 --- /dev/null +++ b/apps/web-vue/src/lib/api/payments.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; + +export interface CreatePaymentLinkRequest { + orderId: string; +} + +export interface CreatePaymentLinkResponse { + paymentUrl: string; + transactionCode: string; +} + +export async function createPaymentLink( + data: CreatePaymentLinkRequest, +): Promise { + const response = await apiClient.post<{ data: CreatePaymentLinkResponse }>( + '/v1/payments/create-link', + data, + ); + return response.data.data; +} + +export async function verifyPayment(transactionCode: string): Promise<{ status: string }> { + const response = await apiClient.get<{ data: { status: string } }>( + `/v1/payments/verify/${transactionCode}`, + ); + return response.data.data; +} diff --git a/apps/web-vue/src/lib/api/products.ts b/apps/web-vue/src/lib/api/products.ts new file mode 100644 index 0000000..acdb20e --- /dev/null +++ b/apps/web-vue/src/lib/api/products.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; + +export type ProductSlug = 'AO_THUN' | 'PAD_CHUOT' | 'DAY_DEO' | 'MOC_KHOA'; +export type ProductSize = 'S' | 'M' | 'L' | 'XL'; +export type StockStatus = 'in_stock' | 'low_stock' | 'out_of_stock'; + +export interface ProductImage { + id: string; + url: string; + altText: string | null; + order: number; +} + +export interface ProductSizeStock { + size: ProductSize; + stock: number; +} + +export interface Product { + id: string; + slug: ProductSlug; + name: string; + description: string; + priceCurrent: number; + priceOriginal: number | null; + priceDiscount: number | null; + isPublished: boolean; + stock: number; + hasSizes: boolean; + badge: string | null; + ratingValue: number; + ratingCount: number; + images: ProductImage[]; + sizeStocks?: ProductSizeStock[] | null; + stockStatus: StockStatus; + createdAt: string; + updatedAt: string; +} + +export interface ProductListResponse { + products: Product[]; + total: number; +} + +export interface StockInfo { + stock: number; + stockStatus: StockStatus; + sizeStocks?: ProductSizeStock[] | null; +} + +export const productsApi = { + /** + * Get all products + */ + async listProducts(): Promise { + const response = await apiClient.get<{ data: ProductListResponse }>('/products'); + return response.data.data; + }, + + /** + * Get product by slug + */ + async getProductBySlug(slug: ProductSlug): Promise { + const response = await apiClient.get<{ data: Product }>(`/products/${slug}`); + return response.data.data; + }, + + /** + * Get product stock information + */ + async getProductStock(slug: ProductSlug, size?: ProductSize): Promise { + const params = size ? { size } : {}; + const response = await apiClient.get<{ data: StockInfo }>(`/products/${slug}/stock`, { + params, + }); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/reviews.ts b/apps/web-vue/src/lib/api/reviews.ts new file mode 100644 index 0000000..82d6e08 --- /dev/null +++ b/apps/web-vue/src/lib/api/reviews.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '../api-client'; +import type { ProductSlug } from './products'; + +export type { ProductSlug }; + +export interface Review { + id: string; + rating: number; + comment: string | null; + userId: string; + userFullName: string; + userAvatar: string | null; + productId: string; + productSlug: ProductSlug; + createdAt: string; + updatedAt: string; +} + +export interface CreateReviewRequest { + rating: number; + comment?: string | null; +} + +export interface UpdateReviewRequest { + rating?: number; + comment?: string | null; +} + +export interface ReviewListResponse { + reviews: Review[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export const reviewsApi = { + /** + * Get product reviews + */ + async getProductReviews( + productSlug: ProductSlug, + page?: number, + limit?: number, + ): Promise { + const params: Record = {}; + if (page) params.page = page.toString(); + if (limit) params.limit = limit.toString(); + const response = await apiClient.get<{ data: ReviewListResponse }>( + `/products/${productSlug}/reviews`, + { params }, + ); + return response.data.data; + }, + + /** + * Create review + */ + async createReview(productSlug: ProductSlug, data: CreateReviewRequest): Promise { + const response = await apiClient.post<{ data: Review }>( + `/products/${productSlug}/reviews`, + data, + ); + return response.data.data; + }, + + /** + * Update review + */ + async updateReview(reviewId: string, data: UpdateReviewRequest): Promise { + const response = await apiClient.patch<{ data: Review }>(`/products/reviews/${reviewId}`, data); + return response.data.data; + }, + + /** + * Delete review + */ + async deleteReview(reviewId: string): Promise<{ success: boolean }> { + const response = await apiClient.delete<{ data: { success: boolean } }>( + `/products/reviews/${reviewId}`, + ); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/api/users.ts b/apps/web-vue/src/lib/api/users.ts new file mode 100644 index 0000000..deda8d5 --- /dev/null +++ b/apps/web-vue/src/lib/api/users.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { apiClient } from '@/lib/api-client'; +import type { User } from '@/types/auth.types'; + +export type UserRole = 'ADMIN' | 'CUSTOMER'; + +export interface UpdateProfileRequest { + fullName?: string; + phone?: string; +} + +export interface UpdateProfileResponse { + id: string; + email: string; + fullName: string | null; + phone: string | null; + avatar: string | null; + role: UserRole; + emailVerified: boolean; + phoneVerified: boolean; + mfaEnabled: boolean; + createdAt: Date; + updatedAt: Date; +} + +export const usersApi = { + /** + * Get current user profile + */ + async getUserProfile(): Promise { + const response = await apiClient.get<{ data: User }>('/users/me'); + return response.data.data; + }, + + /** + * Update user profile (fullName, phone) + */ + async updateProfile(data: UpdateProfileRequest): Promise { + const response = await apiClient.patch<{ data: UpdateProfileResponse }>('/users/profile', data); + return response.data.data; + }, + + /** + * Update user avatar + */ + async updateAvatar(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + + const response = await apiClient.patch<{ data: UpdateProfileResponse }>( + '/users/avatar', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + return response.data.data; + }, +}; diff --git a/apps/web-vue/src/lib/csrf.ts b/apps/web-vue/src/lib/csrf.ts new file mode 100644 index 0000000..b56a5c2 --- /dev/null +++ b/apps/web-vue/src/lib/csrf.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import axios from 'axios'; + +import { API_URL } from '@/lib/constants'; + +let csrfToken: string | null = null; +let tokenPromise: Promise | null = null; + +/** + * Fetch CSRF token from API + * Uses caching to avoid multiple simultaneous requests + */ +export async function getCsrfToken(): Promise { + // Return cached token if available + if (csrfToken) { + return csrfToken; + } + + // If already fetching, return the existing promise + if (tokenPromise) { + return tokenPromise; + } + + // Fetch new token + tokenPromise = axios + .get<{ csrfToken: string }>(`${API_URL}/csrf-token`, { + withCredentials: true, // Include cookies for session + }) + .then((response) => { + csrfToken = response.data.csrfToken; + return csrfToken; + }) + .catch(() => { + throw new Error('Failed to fetch CSRF token'); + }) + .finally(() => { + tokenPromise = null; + }); + + return tokenPromise; +} + +/** + * Clear cached CSRF token + * Useful when session changes or token expires + */ +export function clearCsrfToken(): void { + csrfToken = null; + tokenPromise = null; +} + +/** + * Get current CSRF token (synchronous) + * Returns null if token hasn't been fetched yet + */ +export function getCurrentCsrfToken(): string | null { + return csrfToken; +} diff --git a/apps/web-vue/src/lib/events/mock-events.ts b/apps/web-vue/src/lib/events/mock-events.ts new file mode 100644 index 0000000..f633805 --- /dev/null +++ b/apps/web-vue/src/lib/events/mock-events.ts @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +// ponytail: date-fns not in web-vue deps; minimal helpers replace addDays/addHours/addMonths/addWeeks/subDays/subWeeks +const DAY = 864e5; +const addHours = (d: Date, n: number) => new Date(d.getTime() + n * 36e5); +const addDays = (d: Date, n: number) => new Date(d.getTime() + n * DAY); +const addWeeks = (d: Date, n: number) => addDays(d, n * 7); +const subDays = (d: Date, n: number) => addDays(d, -n); +const subWeeks = (d: Date, n: number) => addDays(d, -n * 7); +const addMonths = (d: Date, n: number) => { + const x = new Date(d); + x.setMonth(x.getMonth() + n); + return x; +}; + +import { EventType, type Event } from './types'; + +const now = new Date(); +const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + +export const mockEvents: Event[] = [ + // Past events + { + id: '1', + title: 'Cuộc họp Ban Chủ nhiệm tháng 1', + description: + 'Cuộc họp định kỳ của Ban Chủ nhiệm để đánh giá hoạt động tháng vừa qua và lên kế hoạch cho tháng tới.', + start: subWeeks(today, 4), + end: addHours(subWeeks(today, 4), 2), + location: 'Phòng họp A1.1', + type: EventType.MEETING, + organizer: 'Ban Chủ nhiệm', + attendees: 15, + }, + { + id: '2', + title: 'Workshop: Next.js 16 App Router', + description: + 'Workshop giới thiệu về Next.js 16 với App Router, Server Components, và các tính năng mới nhất.', + start: subWeeks(today, 3), + end: addHours(subWeeks(today, 3), 3), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Minh - Tech Lead', + attendees: 45, + }, + { + id: '3', + title: 'Khảo sát: Đánh giá hoạt động câu lạc bộ Q1', + description: + 'Khảo sát để thu thập ý kiến của thành viên về các hoạt động trong quý 1, giúp cải thiện chất lượng hoạt động.', + start: subWeeks(today, 2), + end: addDays(subWeeks(today, 2), 7), + type: EventType.SURVEY, + organizer: 'Ban Chủ nhiệm', + surveyLink: 'https://forms.gle/example1', + }, + { + id: '4', + title: 'Cuộc thi Hackathon WebDev 2024', + description: + 'Cuộc thi lập trình web dành cho thành viên câu lạc bộ. Thể lệ: làm việc nhóm, thời gian 48 giờ.', + start: subWeeks(today, 1), + end: addDays(subWeeks(today, 1), 2), + location: 'Phòng lab CNTT', + type: EventType.COMPETITION, + organizer: 'Ban Tổ chức', + attendees: 60, + }, + { + id: '5', + title: 'Teambuilding: Game night', + description: + 'Buổi tối chơi game cùng nhau để gắn kết thành viên. Các trò chơi: Among Us, Codenames, Pictionary.', + start: subDays(today, 5), + end: addHours(subDays(today, 5), 4), + location: 'Phòng sinh hoạt chung', + type: EventType.SOCIAL, + organizer: 'Ban Sự kiện', + attendees: 30, + }, + + // Current/Upcoming events + { + id: '6', + title: 'Khảo sát: Nhu cầu học tập của thành viên', + description: + 'Khảo sát để nắm bắt nhu cầu học tập và phát triển kỹ năng của thành viên, từ đó xây dựng chương trình đào tạo phù hợp.', + start: subDays(today, 2), + end: addDays(today, 5), + type: EventType.SURVEY, + organizer: 'Ban Đào tạo', + surveyLink: 'https://forms.gle/example2', + }, + { + id: '7', + title: 'Workshop: TypeScript Advanced Patterns', + description: + 'Workshop nâng cao về TypeScript: Generics, Utility Types, Conditional Types, và các pattern thực tế trong dự án.', + start: addDays(today, 2), + end: addHours(addDays(today, 2), 3), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Hùng - Senior Developer', + attendees: 0, + }, + { + id: '8', + title: 'Cuộc họp Ban Chủ nhiệm tháng hiện tại', + description: 'Cuộc họp định kỳ để đánh giá tiến độ các dự án và hoạt động đang diễn ra.', + start: addDays(today, 3), + end: addHours(addDays(today, 3), 2), + location: 'Phòng họp A1.1', + type: EventType.MEETING, + organizer: 'Ban Chủ nhiệm', + attendees: 0, + }, + { + id: '9', + title: 'Workshop: React Server Components', + description: + 'Tìm hiểu về React Server Components, cách sử dụng trong Next.js, và best practices.', + start: addDays(today, 5), + end: addHours(addDays(today, 5), 2.5), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Đức - Full-stack Developer', + attendees: 0, + }, + { + id: '10', + title: 'Khảo sát: Đánh giá Workshop vừa qua', + description: + 'Khảo sát nhanh để thu thập feedback về workshop TypeScript Advanced Patterns, giúp cải thiện chất lượng các workshop tiếp theo.', + start: addDays(today, 4), + end: addDays(today, 7), + type: EventType.SURVEY, + organizer: 'Ban Đào tạo', + surveyLink: 'https://forms.gle/example3', + }, + { + id: '11', + title: 'Social: Coffee & Code', + description: + 'Buổi gặp mặt không chính thức, cùng nhau uống cà phê và chia sẻ về các dự án đang làm, học hỏi lẫn nhau.', + start: addDays(today, 6), + end: addHours(addDays(today, 6), 2), + location: 'Café gần trường', + type: EventType.SOCIAL, + organizer: 'Thành viên tự tổ chức', + attendees: 0, + }, + { + id: '12', + title: 'Workshop: Database Design & Optimization', + description: + 'Workshop về thiết kế database, indexing, query optimization, và best practices với PostgreSQL.', + start: addDays(today, 8), + end: addHours(addDays(today, 8), 3), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Tuấn - Backend Lead', + attendees: 0, + }, + { + id: '13', + title: 'Khảo sát: Lựa chọn chủ đề workshop tiếp theo', + description: 'Khảo sát để thành viên vote cho chủ đề workshop mà họ muốn học trong tháng tới.', + start: addDays(today, 10), + end: addDays(today, 14), + type: EventType.SURVEY, + organizer: 'Ban Đào tạo', + surveyLink: 'https://forms.gle/example4', + }, + { + id: '14', + title: 'Cuộc thi: Code Review Challenge', + description: + 'Cuộc thi review code, tìm bug, và đề xuất cải thiện. Thời gian: 2 giờ. Giải thưởng hấp dẫn.', + start: addDays(today, 12), + end: addHours(addDays(today, 12), 2), + location: 'Phòng lab CNTT', + type: EventType.COMPETITION, + organizer: 'Ban Tổ chức', + attendees: 0, + }, + { + id: '15', + title: 'Cuộc họp Ban Chủ nhiệm tháng tới', + description: 'Cuộc họp định kỳ để lên kế hoạch cho tháng tiếp theo.', + start: addWeeks(today, 4), + end: addHours(addWeeks(today, 4), 2), + location: 'Phòng họp A1.1', + type: EventType.MEETING, + organizer: 'Ban Chủ nhiệm', + attendees: 0, + }, + { + id: '16', + title: 'Workshop: Testing với Jest & React Testing Library', + description: 'Workshop về testing trong React: Unit tests, Integration tests, và E2E testing.', + start: addWeeks(today, 5), + end: addHours(addWeeks(today, 5), 3), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Linh - QA Lead', + attendees: 0, + }, + { + id: '17', + title: 'Social: Movie Night - Tech Documentaries', + description: 'Cùng nhau xem các bộ phim tài liệu về công nghệ và lập trình, sau đó thảo luận.', + start: addWeeks(today, 6), + end: addHours(addWeeks(today, 6), 3), + location: 'Phòng sinh hoạt chung', + type: EventType.SOCIAL, + organizer: 'Ban Sự kiện', + attendees: 0, + }, + { + id: '18', + title: 'Khảo sát: Đánh giá hoạt động câu lạc bộ Q2', + description: 'Khảo sát tổng kết quý 2, thu thập ý kiến để cải thiện hoạt động trong quý 3.', + start: addMonths(today, 2), + end: addDays(addMonths(today, 2), 7), + type: EventType.SURVEY, + organizer: 'Ban Chủ nhiệm', + surveyLink: 'https://forms.gle/example5', + }, + { + id: '19', + title: 'Workshop: DevOps với Docker & CI/CD', + description: + 'Workshop về DevOps: Containerization với Docker, CI/CD pipelines, và deployment strategies.', + start: addWeeks(today, 7), + end: addHours(addWeeks(today, 7), 4), + location: 'Phòng lab CNTT', + type: EventType.WORKSHOP, + organizer: 'Anh Nam - DevOps Engineer', + attendees: 0, + }, + { + id: '20', + title: 'Cuộc thi: WebDev Hackathon 2024 - Final Round', + description: + 'Vòng chung kết cuộc thi Hackathon WebDev 2024. Top 10 đội sẽ trình bày dự án trước ban giám khảo.', + start: addWeeks(today, 8), + end: addDays(addWeeks(today, 8), 1), + location: 'Hội trường lớn', + type: EventType.COMPETITION, + organizer: 'Ban Tổ chức', + attendees: 0, + }, +]; diff --git a/apps/web-vue/src/lib/events/types.ts b/apps/web-vue/src/lib/events/types.ts new file mode 100644 index 0000000..1812093 --- /dev/null +++ b/apps/web-vue/src/lib/events/types.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export enum EventType { + MEETING = 'MEETING', + WORKSHOP = 'WORKSHOP', + SOCIAL = 'SOCIAL', + COMPETITION = 'COMPETITION', + SURVEY = 'SURVEY', + OTHER = 'OTHER', +} + +export interface Event { + id: string; + title: string; + description?: string; + start: Date; + end: Date; + location?: string; + type: EventType; + organizer?: string; + attendees?: number; + surveyLink?: string; // For SURVEY type events +} diff --git a/apps/web-vue/src/lib/hooks/use-debounce.ts b/apps/web-vue/src/lib/hooks/use-debounce.ts new file mode 100644 index 0000000..4c48b91 --- /dev/null +++ b/apps/web-vue/src/lib/hooks/use-debounce.ts @@ -0,0 +1,45 @@ +import { onScopeDispose, ref, watch, type MaybeRefOrGetter } from 'vue'; +import { toValue } from 'vue'; + +export const DEFAULT_DELAY = 500; + +/** + * Debounce a value - returns a ref with the value after delay milliseconds of no changes + */ +export function useDebounce(value: MaybeRefOrGetter, delay: number = DEFAULT_DELAY) { + const debouncedValue = ref(toValue(value)) as ReturnType>; + const timer = { id: null as ReturnType | null }; + const stop = watch( + () => toValue(value), + (v) => { + if (timer.id) clearTimeout(timer.id); + timer.id = setTimeout(() => { + debouncedValue.value = v; + }, delay); + }, + ); + onScopeDispose(() => { + if (timer.id) clearTimeout(timer.id); + stop(); + }); + return debouncedValue; +} + +/** + * Debounce a callback - returned function only executes after delay ms of no calls + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useDebouncedCallback void>( + callback: T, + delay: number = DEFAULT_DELAY, +): T { + let timeoutId: ReturnType | null = null; + const debouncedCallback = ((...args: Parameters) => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => callback(...args), delay); + }) as T; + onScopeDispose(() => { + if (timeoutId) clearTimeout(timeoutId); + }); + return debouncedCallback; +} diff --git a/apps/web-vue/src/lib/metadata.ts b/apps/web-vue/src/lib/metadata.ts new file mode 100644 index 0000000..f1e91bc --- /dev/null +++ b/apps/web-vue/src/lib/metadata.ts @@ -0,0 +1,118 @@ +// Port of apps/web src/lib/metadata.ts: Next `Metadata` objects → unhead head entries. +// Pages call usePageMeta({ title, description, path, image?, keywords? }) — same args as +// createPageMetadata in apps/web — to set title/description/canonical/OG/Twitter tags. +import { useHead, type UseHeadInput } from '@unhead/vue'; + +import { SITE_URL } from './constants'; + +export const siteUrl = SITE_URL; +const siteName = 'WebDev Studios'; + +// SEO Images mapping: route path → SEO image path +export const SEO_IMAGES: Record = { + '/': '/seo/landing.webp', + '/about': '/seo/about.webp', + '/shop': '/seo/shop.webp', + '/shop/ao-thun': '/seo/shop/ao-thun.webp', + '/shop/moc-khoa': '/seo/shop/moc-khoa.webp', + '/shop/day-deo': '/seo/shop/day-deo.webp', + '/shop/pad-chuot': '/seo/shop/pad-chuot.webp', + '/cart': '/seo/shop.webp', + '/generation': '/seo/generation.webp', + '/faq': '/seo/faq.webp', + '/terms': '/seo/legal/terms.webp', + '/privacy': '/seo/legal/privacy.webp', + '/refund': '/seo/legal/refund.webp', +}; +const siteDescription = + 'WebDev Studios là nơi tập hợp các bạn sinh viên có niềm đam mê với Lập trình Web nhằm tạo ra một môi trường học tập và giải trí để các bạn có thể học hỏi, trau dồi kỹ năng và phát triển bản thân.'; +const siteKeywords = [ + 'WebDev Studios', + 'Câu lạc bộ lập trình web', + 'UIT', + 'Sinh viên UIT', + 'Lập trình web', + 'Web development', + 'Frontend', + 'Backend', + 'Fullstack', + 'React', + 'Next.js', + 'JavaScript', + 'TypeScript', + 'Cộng đồng lập trình', + 'Học lập trình', +]; + +// unhead has no Next-style title.template — createPageMetadata already computes "%s | WebDev Studios" +export const defaultMetadata: UseHeadInput = { + title: `${siteName} | Câu lạc bộ lập trình web của sinh viên UIT`, + meta: [ + { name: 'description', content: siteDescription }, + { name: 'keywords', content: siteKeywords.join(', ') }, + { name: 'author', content: siteName }, + { name: 'creator', content: siteName }, + { name: 'publisher', content: siteName }, + { name: 'category', content: 'Education' }, + { name: 'robots', content: 'index, follow' }, + { property: 'og:type', content: 'website' }, + { property: 'og:locale', content: 'vi_VN' }, + { property: 'og:url', content: siteUrl }, + { property: 'og:site_name', content: siteName }, + { property: 'og:title', content: `${siteName} | Câu lạc bộ lập trình web của sinh viên UIT` }, + { property: 'og:description', content: siteDescription }, + { property: 'og:image', content: `${siteUrl}/icon-512x512.png` }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: `${siteName} | Câu lạc bộ lập trình web của sinh viên UIT` }, + { name: 'twitter:description', content: siteDescription }, + { name: 'twitter:image', content: `${siteUrl}/icon-512x512.png` }, + { name: 'twitter:creator', content: '@webdevstudios' }, + ], + link: [{ rel: 'canonical', href: siteUrl }], +}; + +export function createPageMetadata({ + title, + description, + path, + image, + keywords, +}: { + title: string; + description: string; + path: string; + image?: string; + keywords?: string[]; +}): UseHeadInput { + const url = `${siteUrl}${path}`; + const fullTitle = `${title} | ${siteName}`; + const ogImage = image ? `${siteUrl}${image}` : `${siteUrl}/icon-512x512.png`; + + return { + title: fullTitle, + meta: [ + { name: 'description', content: description }, + { name: 'keywords', content: (keywords || siteKeywords).join(', ') }, + { property: 'og:title', content: fullTitle }, + { property: 'og:description', content: description }, + { property: 'og:url', content: url }, + { property: 'og:image', content: ogImage }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: fullTitle }, + { name: 'twitter:description', content: description }, + { name: 'twitter:image', content: ogImage }, + ], + link: [{ rel: 'canonical', href: url }], + }; +} + +/** Page-level head: title/description/canonical/OG/Twitter for the current page. */ +export function usePageMeta(opts: { + title: string; + description: string; + path: string; + image?: string; + keywords?: string[]; +}) { + useHead(createPageMetadata(opts)); +} diff --git a/apps/web-vue/src/lib/product-slug-mapping.ts b/apps/web-vue/src/lib/product-slug-mapping.ts new file mode 100644 index 0000000..ff206ca --- /dev/null +++ b/apps/web-vue/src/lib/product-slug-mapping.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +// Map frontend URL slugs to backend ProductSlug enums +export const URL_SLUG_TO_BACKEND_SLUG: Record< + string, + 'AO_THUN' | 'PAD_CHUOT' | 'DAY_DEO' | 'MOC_KHOA' +> = { + 'ao-thun': 'AO_THUN', + 'pad-chuot': 'PAD_CHUOT', + 'day-deo': 'DAY_DEO', + 'moc-khoa': 'MOC_KHOA', +}; + +export const getBackendSlug = ( + urlSlug: string, +): 'AO_THUN' | 'PAD_CHUOT' | 'DAY_DEO' | 'MOC_KHOA' => { + const backendSlug = URL_SLUG_TO_BACKEND_SLUG[urlSlug]; + if (!backendSlug) { + throw new Error(`Invalid product slug: ${urlSlug}`); + } + return backendSlug; +}; diff --git a/apps/web-vue/src/lib/product-static-content.ts b/apps/web-vue/src/lib/product-static-content.ts new file mode 100644 index 0000000..8afa65b --- /dev/null +++ b/apps/web-vue/src/lib/product-static-content.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import type { ProductSlug } from '@/lib/api/products'; + +export interface ProductImages { + src: string; + alt: string; +} + +export interface ProductStaticContent { + images: ProductImages[]; + features: string[]; + additionalInfo: Record; +} + +export const getProductStaticContent = (slug: ProductSlug): ProductStaticContent => { + const contentMap: Record = { + AO_THUN: { + images: [ + { src: '/shop/ao-thun.webp', alt: 'Áo thun WebDev Studios' }, + { src: '/shop/ao-thun.webp', alt: 'Áo thun WebDev Studios - Ảnh 2' }, + { src: '/shop/ao-thun.webp', alt: 'Áo thun WebDev Studios - Ảnh 3' }, + ], + features: [ + 'Chất liệu cotton 100% mềm mại', + 'Logo in công nghệ cao, bền màu', + 'Size đa dạng từ S đến XL', + 'Thiết kế độc quyền WebDev Studios', + ], + additionalInfo: { + material: 'Cotton 100%', + color: 'Đen với logo cam', + origin: 'Việt Nam', + warranty: 'Đổi trả trong 7 ngày', + }, + }, + PAD_CHUOT: { + images: [ + { + src: '/shop/pad-chuot.webp', + alt: 'Pad chuột WebDev Studios Limited Edition', + }, + { + src: '/shop/pad-chuot.webp', + alt: 'Pad chuột WebDev Studios - Ảnh 2', + }, + { + src: '/shop/pad-chuot.webp', + alt: 'Pad chuột WebDev Studios - Ảnh 3', + }, + ], + features: [ + 'Phiên bản Limited Edition độc quyền', + 'Kích thước lớn phù hợp cho developer', + 'Bề mặt mịn màng, độ nhạy cao', + 'Chống mài mòn, bền đẹp theo thời gian', + 'Thiết kế WebDev Studios độc đáo', + 'Tối ưu cho gaming và design work', + ], + additionalInfo: { + material: 'Cao su cao cấp + vải', + color: 'Đen với logo cam', + origin: 'Việt Nam', + warranty: 'Bảo hành 6 tháng', + size: '800mm x 300mm', + thickness: '3mm', + }, + }, + DAY_DEO: { + images: [ + { src: '/shop/day-deo.webp', alt: 'Dây đeo WebDev Studios' }, + { src: '/shop/day-deo.webp', alt: 'Dây đeo WebDev Studios - Ảnh 2' }, + { src: '/shop/day-deo.webp', alt: 'Dây đeo WebDev Studios - Ảnh 3' }, + ], + features: [ + 'Thiết kế nằm ngang với branding WebDev Studios', + 'Chất liệu polyester cao cấp, bền chắc', + 'Phù hợp cho thẻ nhân viên, thẻ hội viên', + 'Có thể dùng làm keychain tiện lợi', + 'Màu sắc chuyên nghiệp, dễ phối hợp', + 'Sản phẩm độc quyền cho thành viên WDS', + ], + additionalInfo: { + material: 'Polyester cao cấp', + color: 'Đen với logo cam', + origin: 'Việt Nam', + warranty: 'Bảo hành 3 tháng', + width: '25mm', + length: '90cm', + }, + }, + MOC_KHOA: { + images: [ + { src: '/shop/moc-khoa.webp', alt: 'Móc khóa WebDev Studios' }, + { src: '/shop/moc-khoa.webp', alt: 'Móc khóa WebDev Studios - Ảnh 2' }, + { src: '/shop/moc-khoa.webp', alt: 'Móc khóa WebDev Studios - Ảnh 3' }, + ], + features: [ + 'Chất liệu kim loại cao cấp, bền chắc', + 'Logo WebDev Studios được khắc tinh xảo', + 'Thiết kế độc đáo, nhỏ gọn', + 'Phù hợp treo chìa khóa, túi xách', + 'Có thể dùng làm vật trang trí', + 'Sản phẩm độc quyền cho thành viên WDS', + ], + additionalInfo: { + material: 'Kim loại cao cấp', + color: 'Vàng đồng', + origin: 'Việt Nam', + warranty: 'Bảo hành 1 năm', + size: 'Kích thước 5cm x 3cm', + }, + }, + }; + + return contentMap[slug]; +}; diff --git a/apps/web-vue/src/lib/query-client.ts b/apps/web-vue/src/lib/query-client.ts new file mode 100644 index 0000000..4f0bb34 --- /dev/null +++ b/apps/web-vue/src/lib/query-client.ts @@ -0,0 +1,14 @@ +import { QueryClient } from '@tanstack/vue-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + refetchOnWindowFocus: false, + retry: 1, + }, + mutations: { + retry: 0, + }, + }, +}); diff --git a/apps/web-vue/src/lib/shipping.ts b/apps/web-vue/src/lib/shipping.ts new file mode 100644 index 0000000..209538a --- /dev/null +++ b/apps/web-vue/src/lib/shipping.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository on GitHub + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export const FREE_SHIPPING_THRESHOLD = 500000; +export const SHIPPING_FEE = 30000; + +// ponytail: mirrors backend fee rules (NestJS orders.service, Go orders service, Elysia lib/orders) — keep in sync +export function shippingFee(subtotal: number): number { + return subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE; +} + +export function isFreeShipping(subtotal: number): boolean { + return subtotal >= FREE_SHIPPING_THRESHOLD; +} diff --git a/apps/web-vue/src/lib/structured-data.ts b/apps/web-vue/src/lib/structured-data.ts new file mode 100644 index 0000000..47c62c5 --- /dev/null +++ b/apps/web-vue/src/lib/structured-data.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +import { siteUrl } from './metadata'; + +export interface OrganizationSchema { + '@context': string; + '@type': string; + name: string; + url: string; + logo: string; + description: string; + sameAs?: string[]; + contactPoint?: { + '@type': string; + contactType: string; + email?: string; + url?: string; + }; +} + +export interface WebSiteSchema { + '@context': string; + '@type': string; + name: string; + url: string; + description: string; + potentialAction?: { + '@type': string; + target: { + '@type': string; + urlTemplate: string; + }; + 'query-input': string; + }; +} + +export interface BreadcrumbSchema { + '@context': string; + '@type': string; + itemListElement: Array<{ + '@type': string; + position: number; + name: string; + item?: string; + }>; +} + +export function getOrganizationSchema(): OrganizationSchema { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + name: 'WebDev Studios', + url: siteUrl, + logo: `${siteUrl}/icons/icon-512x512.png`, + description: + 'WebDev Studios là nơi tập hợp các bạn sinh viên có niềm đam mê với Lập trình Web nhằm tạo ra một môi trường học tập và giải trí để các bạn có thể học hỏi, trau dồi kỹ năng và phát triển bản thân.', + sameAs: [ + // Add social media links here + // 'https://www.facebook.com/webdevstudios', + // 'https://twitter.com/webdevstudios', + // 'https://github.com/webdevstudios', + ], + contactPoint: { + '@type': 'ContactPoint', + contactType: 'Customer Service', + // email: 'contact@webdevstudios.org', + // url: `${siteUrl}/contact`, + }, + }; +} + +export function getWebSiteSchema(): WebSiteSchema { + return { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'WebDev Studios', + url: siteUrl, + description: + 'WebDev Studios là nơi tập hợp các bạn sinh viên có niềm đam mê với Lập trình Web nhằm tạo ra một môi trường học tập và giải trí để các bạn có thể học hỏi, trau dồi kỹ năng và phát triển bản thân.', + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${siteUrl}/search?q={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + }; +} + +export function getBreadcrumbSchema( + items: Array<{ name: string; url?: string }>, +): BreadcrumbSchema { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + ...(item.url && { item: item.url }), + })), + }; +} + +export function getPageSchema({ + title, + description, + url, + image, + datePublished, + dateModified, +}: { + title: string; + description: string; + url: string; + image?: string; + datePublished?: string; + dateModified?: string; +}) { + return { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: title, + description, + url, + ...(image && { + image: image.startsWith('http') ? image : `${siteUrl}${image}`, + }), + ...(datePublished && { datePublished }), + ...(dateModified && { dateModified }), + publisher: getOrganizationSchema(), + }; +} diff --git a/apps/web-vue/src/lib/toast.ts b/apps/web-vue/src/lib/toast.ts new file mode 100644 index 0000000..2de9e70 --- /dev/null +++ b/apps/web-vue/src/lib/toast.ts @@ -0,0 +1,28 @@ +// ponytail: sonner is React-only and not in web-vue deps (no new deps allowed) — minimal DOM +// toast with sonner's call surface, top-center like apps/web's . +// Swap this file's body for vue-sonner (+ in App.vue) when a dep is allowed. +type ToastType = 'success' | 'error' | 'info' | 'warning'; + +const colors: Record = { + success: '#16a34a', + error: '#dc2626', + info: '#2563eb', + warning: '#d97706', +}; + +function show(message: string, type: ToastType) { + const el = document.createElement('div'); + el.setAttribute('role', 'status'); + el.textContent = message; + el.style.cssText = `position:fixed;top:1rem;left:50%;transform:translateX(-50%);z-index:9999;max-width:90vw;padding:0.75rem 1rem;border-radius:0.5rem;color:#fff;font-size:0.875rem;background:${colors[type]}`; + document.body.appendChild(el); + setTimeout(() => el.remove(), 4000); +} + +export const toast = { + success: (message: string) => show(message, 'success'), + error: (message: string) => show(message, 'error'), + info: (message: string) => show(message, 'info'), + warning: (message: string) => show(message, 'warning'), + message: (message: string) => show(message, 'info'), +}; diff --git a/apps/web-vue/src/lib/utils.ts b/apps/web-vue/src/lib/utils.ts new file mode 100644 index 0000000..9561998 --- /dev/null +++ b/apps/web-vue/src/lib/utils.ts @@ -0,0 +1,10 @@ +// cn lives in @/lib/cn (single cn, T1) — this file carries the rest of apps/web src/lib/utils.ts + +/** + * Format price to Vietnamese currency format + * @param amount - Price amount in VND + * @returns Formatted price string (e.g., "1.000.000") + */ +export function formatPrice(amount: number): string { + return new Intl.NumberFormat('vi-VN').format(amount); +} diff --git a/apps/web-vue/src/lib/utils/avatar.ts b/apps/web-vue/src/lib/utils/avatar.ts new file mode 100644 index 0000000..8c9f337 --- /dev/null +++ b/apps/web-vue/src/lib/utils/avatar.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export function getAvatarInitials(fullName: string | null, email: string): string { + if (fullName) { + const parts = fullName.trim().split(/\s+/); + if (parts.length >= 2) { + // Get first letter of first word and last word + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + // If only one word, get first 2 letters + return fullName.substring(0, 2).toUpperCase(); + } + + // Fallback to email + if (email && email.length > 0) { + return email[0].toUpperCase(); + } + + // Ultimate fallback + return 'U'; +} diff --git a/apps/web-vue/src/main.ts b/apps/web-vue/src/main.ts index 9ab2794..af16d09 100644 --- a/apps/web-vue/src/main.ts +++ b/apps/web-vue/src/main.ts @@ -1,14 +1,15 @@ -import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; +import { VueQueryPlugin } from '@tanstack/vue-query'; import { createHead } from '@unhead/vue/client'; import { createApp } from 'vue'; import App from '@/App.vue'; import '@/assets/globals.css'; +import { queryClient } from '@/lib/query-client'; import { router } from '@/router'; const app = createApp(App); app.use(createHead()); -app.use(VueQueryPlugin, { queryClient: new QueryClient() }); +app.use(VueQueryPlugin, { queryClient }); app.use(router); app.mount('#app'); diff --git a/apps/web-vue/src/pages/Home.vue b/apps/web-vue/src/pages/Home.vue deleted file mode 100644 index 0203ec6..0000000 --- a/apps/web-vue/src/pages/Home.vue +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/apps/web-vue/src/pages/about.vue b/apps/web-vue/src/pages/about.vue new file mode 100644 index 0000000..7353d71 --- /dev/null +++ b/apps/web-vue/src/pages/about.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/account/profile.vue b/apps/web-vue/src/pages/account/profile.vue new file mode 100644 index 0000000..1cece3b --- /dev/null +++ b/apps/web-vue/src/pages/account/profile.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/account/settings.vue b/apps/web-vue/src/pages/account/settings.vue new file mode 100644 index 0000000..2272338 --- /dev/null +++ b/apps/web-vue/src/pages/account/settings.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/achievements.vue b/apps/web-vue/src/pages/achievements.vue new file mode 100644 index 0000000..ba92049 --- /dev/null +++ b/apps/web-vue/src/pages/achievements.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/activities.vue b/apps/web-vue/src/pages/activities.vue new file mode 100644 index 0000000..4353f81 --- /dev/null +++ b/apps/web-vue/src/pages/activities.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/blog/[id].vue b/apps/web-vue/src/pages/admin/blog/[id].vue new file mode 100644 index 0000000..36c35f3 --- /dev/null +++ b/apps/web-vue/src/pages/admin/blog/[id].vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/blog/index.vue b/apps/web-vue/src/pages/admin/blog/index.vue new file mode 100644 index 0000000..324bdde --- /dev/null +++ b/apps/web-vue/src/pages/admin/blog/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/blog/new.vue b/apps/web-vue/src/pages/admin/blog/new.vue new file mode 100644 index 0000000..773c51f --- /dev/null +++ b/apps/web-vue/src/pages/admin/blog/new.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/index.vue b/apps/web-vue/src/pages/admin/index.vue new file mode 100644 index 0000000..1cd1b71 --- /dev/null +++ b/apps/web-vue/src/pages/admin/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/orders.vue b/apps/web-vue/src/pages/admin/orders.vue new file mode 100644 index 0000000..641a2bf --- /dev/null +++ b/apps/web-vue/src/pages/admin/orders.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/products.vue b/apps/web-vue/src/pages/admin/products.vue new file mode 100644 index 0000000..d1af658 --- /dev/null +++ b/apps/web-vue/src/pages/admin/products.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/transactions.vue b/apps/web-vue/src/pages/admin/transactions.vue new file mode 100644 index 0000000..d472d16 --- /dev/null +++ b/apps/web-vue/src/pages/admin/transactions.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/admin/users.vue b/apps/web-vue/src/pages/admin/users.vue new file mode 100644 index 0000000..bdd7422 --- /dev/null +++ b/apps/web-vue/src/pages/admin/users.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/2fa.vue b/apps/web-vue/src/pages/auth/2fa.vue new file mode 100644 index 0000000..29142d8 --- /dev/null +++ b/apps/web-vue/src/pages/auth/2fa.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/forgot-password.vue b/apps/web-vue/src/pages/auth/forgot-password.vue new file mode 100644 index 0000000..1a93dd7 --- /dev/null +++ b/apps/web-vue/src/pages/auth/forgot-password.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/login.vue b/apps/web-vue/src/pages/auth/login.vue new file mode 100644 index 0000000..10ef5e7 --- /dev/null +++ b/apps/web-vue/src/pages/auth/login.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/oauth-callback.vue b/apps/web-vue/src/pages/auth/oauth-callback.vue new file mode 100644 index 0000000..0ab21df --- /dev/null +++ b/apps/web-vue/src/pages/auth/oauth-callback.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/reset-password.vue b/apps/web-vue/src/pages/auth/reset-password.vue new file mode 100644 index 0000000..a419274 --- /dev/null +++ b/apps/web-vue/src/pages/auth/reset-password.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/signup.vue b/apps/web-vue/src/pages/auth/signup.vue new file mode 100644 index 0000000..70cd4c1 --- /dev/null +++ b/apps/web-vue/src/pages/auth/signup.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/auth/verify-email.vue b/apps/web-vue/src/pages/auth/verify-email.vue new file mode 100644 index 0000000..da58dee --- /dev/null +++ b/apps/web-vue/src/pages/auth/verify-email.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/blog/[slug].vue b/apps/web-vue/src/pages/blog/[slug].vue new file mode 100644 index 0000000..77f6851 --- /dev/null +++ b/apps/web-vue/src/pages/blog/[slug].vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/blog/index.vue b/apps/web-vue/src/pages/blog/index.vue new file mode 100644 index 0000000..965f2c5 --- /dev/null +++ b/apps/web-vue/src/pages/blog/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/calendar.vue b/apps/web-vue/src/pages/calendar.vue new file mode 100644 index 0000000..45eeca9 --- /dev/null +++ b/apps/web-vue/src/pages/calendar.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/cart.vue b/apps/web-vue/src/pages/cart.vue new file mode 100644 index 0000000..e17ece5 --- /dev/null +++ b/apps/web-vue/src/pages/cart.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/checkout.vue b/apps/web-vue/src/pages/checkout.vue new file mode 100644 index 0000000..88d99f0 --- /dev/null +++ b/apps/web-vue/src/pages/checkout.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/faq.vue b/apps/web-vue/src/pages/faq.vue new file mode 100644 index 0000000..a6bd41c --- /dev/null +++ b/apps/web-vue/src/pages/faq.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/generation.vue b/apps/web-vue/src/pages/generation.vue new file mode 100644 index 0000000..c7be87a --- /dev/null +++ b/apps/web-vue/src/pages/generation.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/home.vue b/apps/web-vue/src/pages/home.vue new file mode 100644 index 0000000..3e71242 --- /dev/null +++ b/apps/web-vue/src/pages/home.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/legal/privacy.vue b/apps/web-vue/src/pages/legal/privacy.vue new file mode 100644 index 0000000..050e418 --- /dev/null +++ b/apps/web-vue/src/pages/legal/privacy.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/legal/refund.vue b/apps/web-vue/src/pages/legal/refund.vue new file mode 100644 index 0000000..d915734 --- /dev/null +++ b/apps/web-vue/src/pages/legal/refund.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/legal/terms.vue b/apps/web-vue/src/pages/legal/terms.vue new file mode 100644 index 0000000..373ff00 --- /dev/null +++ b/apps/web-vue/src/pages/legal/terms.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/not-found.vue b/apps/web-vue/src/pages/not-found.vue new file mode 100644 index 0000000..fe604de --- /dev/null +++ b/apps/web-vue/src/pages/not-found.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/orders/[id].vue b/apps/web-vue/src/pages/orders/[id].vue new file mode 100644 index 0000000..773a7b3 --- /dev/null +++ b/apps/web-vue/src/pages/orders/[id].vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/orders/index.vue b/apps/web-vue/src/pages/orders/index.vue new file mode 100644 index 0000000..3c5a2a0 --- /dev/null +++ b/apps/web-vue/src/pages/orders/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/partner.vue b/apps/web-vue/src/pages/partner.vue new file mode 100644 index 0000000..81f0ce9 --- /dev/null +++ b/apps/web-vue/src/pages/partner.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/payments/[orderId].vue b/apps/web-vue/src/pages/payments/[orderId].vue new file mode 100644 index 0000000..0259ddf --- /dev/null +++ b/apps/web-vue/src/pages/payments/[orderId].vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/payments/cancel.vue b/apps/web-vue/src/pages/payments/cancel.vue new file mode 100644 index 0000000..0db26f3 --- /dev/null +++ b/apps/web-vue/src/pages/payments/cancel.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/payments/return.vue b/apps/web-vue/src/pages/payments/return.vue new file mode 100644 index 0000000..de700e4 --- /dev/null +++ b/apps/web-vue/src/pages/payments/return.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/shop/ao-thun.vue b/apps/web-vue/src/pages/shop/ao-thun.vue new file mode 100644 index 0000000..9249943 --- /dev/null +++ b/apps/web-vue/src/pages/shop/ao-thun.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/shop/day-deo.vue b/apps/web-vue/src/pages/shop/day-deo.vue new file mode 100644 index 0000000..e1a2e25 --- /dev/null +++ b/apps/web-vue/src/pages/shop/day-deo.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/shop/index.vue b/apps/web-vue/src/pages/shop/index.vue new file mode 100644 index 0000000..b4c1f80 --- /dev/null +++ b/apps/web-vue/src/pages/shop/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/shop/moc-khoa.vue b/apps/web-vue/src/pages/shop/moc-khoa.vue new file mode 100644 index 0000000..47e978d --- /dev/null +++ b/apps/web-vue/src/pages/shop/moc-khoa.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/pages/shop/pad-chuot.vue b/apps/web-vue/src/pages/shop/pad-chuot.vue new file mode 100644 index 0000000..27da776 --- /dev/null +++ b/apps/web-vue/src/pages/shop/pad-chuot.vue @@ -0,0 +1,6 @@ + + + diff --git a/apps/web-vue/src/router/index.ts b/apps/web-vue/src/router/index.ts index fa7eeaa..33374cd 100644 --- a/apps/web-vue/src/router/index.ts +++ b/apps/web-vue/src/router/index.ts @@ -1,8 +1,139 @@ -import { createRouter, createWebHistory } from 'vue-router'; +import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; -import Home from '@/pages/Home.vue'; +import { authStoreReady } from '@/composables/use-auth'; +import About from '@/pages/about.vue'; +import AccountProfile from '@/pages/account/profile.vue'; +import AccountSettings from '@/pages/account/settings.vue'; +import Achievements from '@/pages/achievements.vue'; +import Activities from '@/pages/activities.vue'; +import AdminBlogEdit from '@/pages/admin/blog/[id].vue'; +import AdminBlog from '@/pages/admin/blog/index.vue'; +import AdminBlogNew from '@/pages/admin/blog/new.vue'; +import Admin from '@/pages/admin/index.vue'; +import AdminOrders from '@/pages/admin/orders.vue'; +import AdminProducts from '@/pages/admin/products.vue'; +import AdminTransactions from '@/pages/admin/transactions.vue'; +import AdminUsers from '@/pages/admin/users.vue'; +import Auth2fa from '@/pages/auth/2fa.vue'; +import AuthForgotPassword from '@/pages/auth/forgot-password.vue'; +import AuthLogin from '@/pages/auth/login.vue'; +import AuthOauthCallback from '@/pages/auth/oauth-callback.vue'; +import AuthResetPassword from '@/pages/auth/reset-password.vue'; +import AuthSignup from '@/pages/auth/signup.vue'; +import AuthVerifyEmail from '@/pages/auth/verify-email.vue'; +import Blog from '@/pages/blog/index.vue'; +import BlogPost from '@/pages/blog/[slug].vue'; +import Calendar from '@/pages/calendar.vue'; +import Cart from '@/pages/cart.vue'; +import Checkout from '@/pages/checkout.vue'; +import Faq from '@/pages/faq.vue'; +import Generation from '@/pages/generation.vue'; +import Home from '@/pages/home.vue'; +import LegalPrivacy from '@/pages/legal/privacy.vue'; +import LegalRefund from '@/pages/legal/refund.vue'; +import LegalTerms from '@/pages/legal/terms.vue'; +import NotFound from '@/pages/not-found.vue'; +import Orders from '@/pages/orders/index.vue'; +import OrderDetail from '@/pages/orders/[id].vue'; +import Partner from '@/pages/partner.vue'; +import PaymentsCancel from '@/pages/payments/cancel.vue'; +import PaymentsReturn from '@/pages/payments/return.vue'; +import PaymentDetail from '@/pages/payments/[orderId].vue'; +import ShopAoThun from '@/pages/shop/ao-thun.vue'; +import ShopDayDeo from '@/pages/shop/day-deo.vue'; +import Shop from '@/pages/shop/index.vue'; +import ShopMocKhoa from '@/pages/shop/moc-khoa.vue'; +import ShopPadChuot from '@/pages/shop/pad-chuot.vue'; + +declare module 'vue-router' { + interface RouteMeta { + /** mirrors apps/web app/admin/layout.tsx guard */ + admin?: boolean; + } +} + +// Same URL paths as apps/web; dynamic params use the apps/web segment names. +// meta.admin is the ONLY guard apps/web has (admin layout); every other route is +// public and its pages self-redirect on 401 — do not add auth gates they don't have. +const routes: RouteRecordRaw[] = [ + { path: '/', name: 'home', component: Home }, + { path: '/about', name: 'about', component: About }, + { path: '/account/profile', name: 'account-profile', component: AccountProfile }, + { path: '/account/settings', name: 'account-settings', component: AccountSettings }, + { path: '/achievements', name: 'achievements', component: Achievements }, + { path: '/activities', name: 'activities', component: Activities }, + { path: '/admin', name: 'admin', component: Admin, meta: { admin: true } }, + { path: '/admin/blog', name: 'admin-blog', component: AdminBlog, meta: { admin: true } }, + { + path: '/admin/blog/new', + name: 'admin-blog-new', + component: AdminBlogNew, + meta: { admin: true }, + }, + { + path: '/admin/blog/:id', + name: 'admin-blog-edit', + component: AdminBlogEdit, + meta: { admin: true }, + }, + { path: '/admin/orders', name: 'admin-orders', component: AdminOrders, meta: { admin: true } }, + { + path: '/admin/products', + name: 'admin-products', + component: AdminProducts, + meta: { admin: true }, + }, + { + path: '/admin/transactions', + name: 'admin-transactions', + component: AdminTransactions, + meta: { admin: true }, + }, + { path: '/admin/users', name: 'admin-users', component: AdminUsers, meta: { admin: true } }, + { path: '/auth/2fa', name: 'auth-2fa', component: Auth2fa }, + { path: '/auth/forgot-password', name: 'auth-forgot-password', component: AuthForgotPassword }, + { path: '/auth/login', name: 'auth-login', component: AuthLogin }, + { path: '/auth/oauth/callback', name: 'auth-oauth-callback', component: AuthOauthCallback }, + { path: '/auth/reset-password', name: 'auth-reset-password', component: AuthResetPassword }, + { path: '/auth/signup', name: 'auth-signup', component: AuthSignup }, + { path: '/auth/verify-email', name: 'auth-verify-email', component: AuthVerifyEmail }, + { path: '/blog', name: 'blog', component: Blog }, + { path: '/blog/:slug', name: 'blog-post', component: BlogPost }, + { path: '/calendar', name: 'calendar', component: Calendar }, + { path: '/cart', name: 'cart', component: Cart }, + { path: '/checkout', name: 'checkout', component: Checkout }, + { path: '/faq', name: 'faq', component: Faq }, + { path: '/generation', name: 'generation', component: Generation }, + { path: '/privacy', name: 'legal-privacy', component: LegalPrivacy }, + { path: '/refund', name: 'legal-refund', component: LegalRefund }, + { path: '/terms', name: 'legal-terms', component: LegalTerms }, + { path: '/orders', name: 'orders', component: Orders }, + { path: '/orders/:id', name: 'order-detail', component: OrderDetail }, + { path: '/partner', name: 'partner', component: Partner }, + { path: '/payments/cancel', name: 'payments-cancel', component: PaymentsCancel }, + { path: '/payments/return', name: 'payments-return', component: PaymentsReturn }, + { path: '/payments/:orderId', name: 'payments-detail', component: PaymentDetail }, + { path: '/shop', name: 'shop', component: Shop }, + { path: '/shop/ao-thun', name: 'shop-ao-thun', component: ShopAoThun }, + { path: '/shop/day-deo', name: 'shop-day-deo', component: ShopDayDeo }, + { path: '/shop/moc-khoa', name: 'shop-moc-khoa', component: ShopMocKhoa }, + { path: '/shop/pad-chuot', name: 'shop-pad-chuot', component: ShopPadChuot }, + { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }, +]; export const router = createRouter({ history: createWebHistory(), - routes: [{ path: '/', component: Home }], + routes, + scrollBehavior: () => ({ top: 0 }), +}); + +// Guard mirrors apps/web app/admin/layout.tsx: while the current-user query is loading it +// renders nothing; !isAuthenticated -> /auth/login; role !== 'ADMIN' -> / +router.beforeEach(async (to) => { + if (!to.meta.admin) return true; + const store = await authStoreReady(); + await store.whenLoaded(); + if (!store.user.value) return '/auth/login'; + if (store.user.value.role !== 'ADMIN') return '/'; + return true; }); diff --git a/apps/web-vue/src/types/auth.types.ts b/apps/web-vue/src/types/auth.types.ts new file mode 100644 index 0000000..cecc7d5 --- /dev/null +++ b/apps/web-vue/src/types/auth.types.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export interface LoginRequest { + email: string; + password: string; + rememberMe?: boolean; +} + +export interface LoginResponse { + accessToken: string; + refreshToken: string; + user: { + id: string; + email: string; + fullName: string | null; + emailVerified: boolean; + mfaEnabled: boolean; + }; + requires2FA?: boolean; +} + +export interface RegisterRequest { + email: string; + password: string; + fullName: string; + phone?: string; +} + +export interface RegisterResponse { + userId: string; +} + +export interface VerifyEmailResponse { + success: boolean; +} + +export interface User { + id: string; + email: string; + fullName: string | null; + phone: string | null; + avatar: string | null; + role: string; + emailVerified: boolean; + phoneVerified: boolean; + mfaEnabled: boolean; + createdAt: Date; + updatedAt: Date; +} + +export type OAuthProvider = 'google' | 'github'; + +export interface RefreshTokenRequest { + refreshToken?: string; +} + +export interface RefreshTokenResponse { + accessToken: string; + refreshToken: string; +} + +export interface RequestPasswordResetRequest { + email: string; +} + +export interface RequestPasswordResetResponse { + success: boolean; +} + +export interface ResetPasswordRequest { + token: string; + newPassword: string; +} + +export interface ResetPasswordResponse { + success: boolean; +} + +export interface Verify2FARequest { + code: string; + sessionId?: string; +} + +export interface Verify2FAResponse { + accessToken?: string; + refreshToken?: string; + user?: { + id: string; + email: string; + fullName: string | null; + emailVerified: boolean; + mfaEnabled: boolean; + }; + verified?: boolean; +} + +export interface Enable2FAResponse { + qrCode: string; + secret: string; + backupCodes: string[]; +} + +export interface Session { + id: string; + device: { + id: string; + name: string | null; + type: string; + lastSeenAt: Date; + } | null; + ipAddress: string | null; + userAgent: string | null; + status: 'ACTIVE' | 'EXPIRED' | 'REVOKED'; + createdAt: Date; + expiresAt: Date; +} diff --git a/apps/web-vue/src/types/product.ts b/apps/web-vue/src/types/product.ts new file mode 100644 index 0000000..fed57f9 --- /dev/null +++ b/apps/web-vue/src/types/product.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2026 Xiro The Dev + * + * Source Available License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to: + * - View and study the Software for educational purposes + * - Fork this repository on GitHub for personal reference + * - Share links to this repository + * + * THE FOLLOWING ARE PROHIBITED: + * - Using the Software in production or commercial applications + * - Copying substantial portions of the Software into other projects + * - Distributing modified versions of the Software + * - Removing or altering copyright notices + * + * For commercial licensing or usage permissions, contact: lethanhtrung.trungle@gmail.com + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + */ + +export type ProductSize = 'S' | 'M' | 'L' | 'XL'; + +export interface ProductImage { + src: string; + alt: string; +} + +export interface ProductPrice { + current: number; + original?: number; + discount?: number; +} + +export interface ProductRating { + value: number; + count: number; +} + +export interface ProductInfo { + material?: string; + color?: string; + origin?: string; + warranty?: string; + [key: string]: string | undefined; +} + +export interface Product { + id: string; + name: string; + slug: string; + description: string; + images: ProductImage[]; + price: ProductPrice; + rating: ProductRating; + features: string[]; + additionalInfo: ProductInfo; + stock: number; + hasSizes?: boolean; + sizes?: ProductSize[]; + badge?: string; +} From c56dd815d8247dd6cb9fa9b12d79af77fa4526fa Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 05:03:57 +0700 Subject: [PATCH 05/31] feat(web-vue): T9 calendar page with FullCalendar (daygrid/timegrid), type filter, event modal --- .../components/calendar/CalendarContainer.vue | 96 +++++++++ .../components/calendar/EventDetailsModal.vue | 107 ++++++++++ .../src/components/calendar/EventFilter.vue | 54 +++++ .../src/components/calendar/calendar.css | 190 ++++++++++++++++++ .../src/components/calendar/event-helpers.ts | 88 ++++++++ apps/web-vue/src/pages/calendar.vue | 24 ++- 6 files changed, 556 insertions(+), 3 deletions(-) create mode 100644 apps/web-vue/src/components/calendar/CalendarContainer.vue create mode 100644 apps/web-vue/src/components/calendar/EventDetailsModal.vue create mode 100644 apps/web-vue/src/components/calendar/EventFilter.vue create mode 100644 apps/web-vue/src/components/calendar/calendar.css create mode 100644 apps/web-vue/src/components/calendar/event-helpers.ts diff --git a/apps/web-vue/src/components/calendar/CalendarContainer.vue b/apps/web-vue/src/components/calendar/CalendarContainer.vue new file mode 100644 index 0000000..083ff5d --- /dev/null +++ b/apps/web-vue/src/components/calendar/CalendarContainer.vue @@ -0,0 +1,96 @@ + + + diff --git a/apps/web-vue/src/components/calendar/EventDetailsModal.vue b/apps/web-vue/src/components/calendar/EventDetailsModal.vue new file mode 100644 index 0000000..570c3ad --- /dev/null +++ b/apps/web-vue/src/components/calendar/EventDetailsModal.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/web-vue/src/components/calendar/EventFilter.vue b/apps/web-vue/src/components/calendar/EventFilter.vue new file mode 100644 index 0000000..c1c4632 --- /dev/null +++ b/apps/web-vue/src/components/calendar/EventFilter.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/web-vue/src/components/calendar/calendar.css b/apps/web-vue/src/components/calendar/calendar.css new file mode 100644 index 0000000..1c71371 --- /dev/null +++ b/apps/web-vue/src/components/calendar/calendar.css @@ -0,0 +1,190 @@ +/* FullCalendar v6 restyle — mirrors apps/web calendar.css (react-big-calendar) look. + Scoped under .wds-calendar so it never leaks into other pages. */ +.wds-calendar .fc { + --wds-border: #e5e7eb; + font-size: 14px; +} + +.wds-calendar .fc .fc-toolbar { + margin-bottom: 16px; + flex-wrap: wrap; + gap: 8px; +} + +@media (min-width: 640px) { + .wds-calendar .fc .fc-toolbar { + margin-bottom: 24px; + gap: 16px; + } +} + +.wds-calendar .fc .fc-toolbar-title { + font-size: 14px; + font-weight: 700; + color: #111827; + text-align: center; +} + +@media (min-width: 640px) { + .wds-calendar .fc .fc-toolbar-title { + font-size: 18px; + } +} + +/* Order: title on its own row first (mobile), inline on sm+ — mirrors rbc-toolbar-label */ +.wds-calendar .fc .fc-toolbar-chunk:last-child { + order: -1; + flex-basis: 100%; + justify-content: center; +} + +@media (min-width: 640px) { + .wds-calendar .fc .fc-toolbar-chunk:last-child { + order: 0; + flex-basis: auto; + justify-content: flex-end; + } +} + +.wds-calendar .fc .fc-button { + background: white; + border: 1px solid #d1d5db; + color: #374151; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; + box-shadow: none; + transition: all 0.2s; +} + +@media (min-width: 640px) { + .wds-calendar .fc .fc-button { + padding: 8px 16px; + font-size: 14px; + } +} + +.wds-calendar .fc .fc-button:hover { + background: #f9fafb; + border-color: #9ca3af; +} + +.wds-calendar .fc .fc-button:focus { + box-shadow: none; +} + +.wds-calendar .fc .fc-button-primary:not(:disabled).fc-button-active, +.wds-calendar .fc .fc-button-primary:not(:disabled):active { + background: #f7931e !important; + border-color: #f7931e !important; + color: white !important; +} + +.wds-calendar .fc .fc-button-primary { + background: white; + border-color: #d1d5db; + color: #374151; +} + +.wds-calendar .fc .fc-daygrid-day.fc-day-today, +.wds-calendar .fc .fc-col-header-cell.fc-day-today { + background-color: #f9fafb; +} + +.wds-calendar .fc .fc-day-other .fc-daygrid-day-top { + color: #9ca3af; +} + +.wds-calendar .fc .fc-daygrid-day-number { + text-align: center; + padding: 8px; + color: #374151; + font-weight: 500; +} + +.wds-calendar .fc .fc-daygrid-day.fc-day-today .fc-daygrid-day-number { + color: #f7931e; + font-weight: 700; +} + +.wds-calendar .fc .fc-col-header-cell-cushion { + padding: 12px 8px; + font-weight: 600; + font-size: 14px; + color: #374151; + text-decoration: none; +} + +.wds-calendar .fc .fc-daygrid-day-frame { + min-height: 84px; +} + +/* Events */ +.wds-calendar .fc .fc-event { + border-radius: 4px; + padding: 2px 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + border: none; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: all 0.2s; +} + +.wds-calendar .fc .fc-event:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.wds-calendar .fc .fc-event-title { + font-weight: 500; +} + +.wds-calendar .fc .fc-daygrid-event { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.wds-calendar .fc .fc-h-event .fc-event-main-frame { + display: block; +} + +.wds-calendar .fc .fc-daygrid-more-link { + color: #6b7280; + font-size: 11px; + padding-left: 4px; +} + +/* Grid borders */ +.wds-calendar .fc .fc-scrollgrid, +.wds-calendar .fc .fc-scrollgrid-section > td, +.wds-calendar .fc .fc-scrollgrid th, +.wds-calendar .fc .fc-scrollgrid td { + border-color: #e5e7eb; +} + +.wds-calendar .fc .fc-timegrid-slot { + border-color: #f3f4f6; +} + +.wds-calendar .fc .fc-timegrid-slot-label-cushion { + font-size: 12px; + color: #6b7280; +} + +@media (max-width: 639px) { + .wds-calendar .fc .fc-col-header-cell-cushion { + padding: 8px 4px; + font-size: 11px; + } + .wds-calendar .fc .fc-event { + font-size: 10px; + padding: 1px 3px; + } + .wds-calendar .fc .fc-daygrid-day-number { + padding: 4px; + font-size: 12px; + } +} diff --git a/apps/web-vue/src/components/calendar/event-helpers.ts b/apps/web-vue/src/components/calendar/event-helpers.ts new file mode 100644 index 0000000..f891df7 --- /dev/null +++ b/apps/web-vue/src/components/calendar/event-helpers.ts @@ -0,0 +1,88 @@ +import { EventType, type Event } from '@/lib/events/types'; + +// ponytail: apps/web imports these from src/lib/events/utils.ts, which was not +// ported to web-vue (lib/events only shipped types + mock). Calendar is the only +// consumer, so the pure presentation helpers live in this slice's own dir. +// date-fns is not installed in web-vue — plain Date + Intl replace formatDistance. + +export function filterEventsByType(events: Event[], types: EventType[]): Event[] { + if (types.length === 0) return events; + return events.filter((event) => types.includes(event.type)); +} + +export function getEventTypeColor(type: EventType): string { + switch (type) { + case EventType.MEETING: + return '#3B82F6'; + case EventType.WORKSHOP: + return '#F7931E'; + case EventType.SOCIAL: + return '#10B981'; + case EventType.COMPETITION: + return '#8B5CF6'; + case EventType.SURVEY: + return '#EC4899'; + case EventType.OTHER: + return '#6B7280'; + default: + return '#6B7280'; + } +} + +export function getEventTypeLabel(type: EventType): string { + switch (type) { + case EventType.MEETING: + return 'Cuộc họp'; + case EventType.WORKSHOP: + return 'Workshop'; + case EventType.SOCIAL: + return 'Sự kiện xã hội'; + case EventType.COMPETITION: + return 'Cuộc thi'; + case EventType.SURVEY: + return 'Khảo sát'; + case EventType.OTHER: + return 'Khác'; + default: + return 'Khác'; + } +} + +const pad = (n: number) => String(n).padStart(2, '0'); +const fmtDate = (d: Date) => `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`; +const fmtTime = (d: Date) => `${pad(d.getHours())}:${pad(d.getMinutes())}`; + +export function formatEventTime(event: Event): string { + const startDate = fmtDate(event.start); + const endDate = fmtDate(event.end); + const range = `${fmtTime(event.start)} - ${fmtTime(event.end)}`; + return startDate === endDate + ? `${startDate} ${range}` + : `${startDate} ${fmtTime(event.start)} - ${endDate} ${fmtTime(event.end)}`; +} + +const rtf = new Intl.RelativeTimeFormat('vi', { numeric: 'auto' }); + +// Replaces date-fns formatDistance(..., { addSuffix: true, locale: vi }). +function distanceToNow(date: Date): string { + const diffMs = date.getTime() - Date.now(); + const abs = Math.abs(diffMs); + const units: [Intl.RelativeTimeFormatUnit, number][] = [ + ['year', 31536e6], + ['month', 2592e6], + ['day', 864e5], + ['hour', 36e5], + ['minute', 6e4], + ]; + for (const [unit, ms] of units) { + if (abs >= ms) return rtf.format(Math.round(diffMs / ms), unit); + } + return rtf.format(Math.round(diffMs / 6e4), 'minute'); +} + +export function getRelativeTime(event: Event): string { + const now = Date.now(); + if (event.start.getTime() > now) return `Bắt đầu ${distanceToNow(event.start)}`; + if (event.end.getTime() > now) return `Đang diễn ra - Kết thúc ${distanceToNow(event.end)}`; + return `Đã kết thúc ${distanceToNow(event.end)}`; +} diff --git a/apps/web-vue/src/pages/calendar.vue b/apps/web-vue/src/pages/calendar.vue index 45eeca9..4c6e9b3 100644 --- a/apps/web-vue/src/pages/calendar.vue +++ b/apps/web-vue/src/pages/calendar.vue @@ -1,6 +1,24 @@ - + From 5b7868233a039c70824c2a8ef21bcca9f68f0609 Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 05:05:22 +0700 Subject: [PATCH 06/31] =?UTF-8?q?feat(web-vue):=20blog=20slice=20=E2=80=94?= =?UTF-8?q?=20/blog=20list=20+=20/blog/[slug]=20post=20(T8=20#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/blog/blog-post-card.vue | 50 +++++++++ .../src/components/blog/blog-post-content.vue | 103 ++++++++++++++++++ .../src/components/blog/blog-post-list.vue | 61 +++++++++++ apps/web-vue/src/components/blog/use-blog.ts | 43 ++++++++ apps/web-vue/src/pages/blog/[slug].vue | 89 ++++++++++++++- apps/web-vue/src/pages/blog/index.vue | 49 ++++++++- 6 files changed, 389 insertions(+), 6 deletions(-) create mode 100644 apps/web-vue/src/components/blog/blog-post-card.vue create mode 100644 apps/web-vue/src/components/blog/blog-post-content.vue create mode 100644 apps/web-vue/src/components/blog/blog-post-list.vue create mode 100644 apps/web-vue/src/components/blog/use-blog.ts diff --git a/apps/web-vue/src/components/blog/blog-post-card.vue b/apps/web-vue/src/components/blog/blog-post-card.vue new file mode 100644 index 0000000..1dbbb6d --- /dev/null +++ b/apps/web-vue/src/components/blog/blog-post-card.vue @@ -0,0 +1,50 @@ + + + diff --git a/apps/web-vue/src/components/blog/blog-post-content.vue b/apps/web-vue/src/components/blog/blog-post-content.vue new file mode 100644 index 0000000..0bccb2d --- /dev/null +++ b/apps/web-vue/src/components/blog/blog-post-content.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/web-vue/src/components/blog/blog-post-list.vue b/apps/web-vue/src/components/blog/blog-post-list.vue new file mode 100644 index 0000000..3c4d4d6 --- /dev/null +++ b/apps/web-vue/src/components/blog/blog-post-list.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/web-vue/src/components/blog/use-blog.ts b/apps/web-vue/src/components/blog/use-blog.ts new file mode 100644 index 0000000..57ffc5c --- /dev/null +++ b/apps/web-vue/src/components/blog/use-blog.ts @@ -0,0 +1,43 @@ +// ponytail: T3 shared hooks dir has no blog.ts and src/lib/** is off-limits to this slice — +// blog vue-query hooks live here until a shared file exists. Same shape as src/lib/api/hooks/*. +import { useQuery } from '@tanstack/vue-query'; +import { computed, toValue, type MaybeRefOrGetter } from 'vue'; + +import { blogApi, type BlogPostListResponse, type BlogPostWithContent } from '@/lib/api/blog'; + +export const blogKeys = { + all: ['blog'] as const, + lists: () => [...blogKeys.all, 'list'] as const, + list: (page: number, pageSize: number, q?: string) => + [...blogKeys.lists(), q, page, pageSize] as const, + details: () => [...blogKeys.all, 'detail'] as const, + detail: (slug: string) => [...blogKeys.details(), slug] as const, +}; + +// mirrors apps/web /blog page.tsx: ?q switches listPosts → searchPosts +export function useBlogPosts( + page: MaybeRefOrGetter, + q?: MaybeRefOrGetter, + pageSize: MaybeRefOrGetter = 10, +) { + return useQuery({ + queryKey: computed(() => + blogKeys.list(toValue(page), toValue(pageSize), toValue(q) || undefined), + ), + queryFn: () => { + const query = toValue(q); + const options = { page: toValue(page), pageSize: toValue(pageSize) }; + return query ? blogApi.searchPosts(query, options) : blogApi.listPosts(options); + }, + staleTime: 5 * 60 * 1000, + }); +} + +export function useBlogPost(slug: MaybeRefOrGetter) { + return useQuery({ + queryKey: computed(() => blogKeys.detail(toValue(slug))), + // api returns the content field when includeContent=true (verified in apps/web [slug] page) + queryFn: () => blogApi.getPostBySlug(toValue(slug), true) as Promise, + staleTime: 5 * 60 * 1000, + }); +} diff --git a/apps/web-vue/src/pages/blog/[slug].vue b/apps/web-vue/src/pages/blog/[slug].vue index 77f6851..dc13448 100644 --- a/apps/web-vue/src/pages/blog/[slug].vue +++ b/apps/web-vue/src/pages/blog/[slug].vue @@ -1,6 +1,89 @@ - + diff --git a/apps/web-vue/src/pages/blog/index.vue b/apps/web-vue/src/pages/blog/index.vue index 965f2c5..945820f 100644 --- a/apps/web-vue/src/pages/blog/index.vue +++ b/apps/web-vue/src/pages/blog/index.vue @@ -1,6 +1,49 @@ - + From df5fe13b68be507ec5d5a381f74acc77e04ecf5a Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 05:09:57 +0700 Subject: [PATCH 07/31] =?UTF-8?q?feat(web-vue):=20auth=20pages=20=E2=80=94?= =?UTF-8?q?=20login,=20signup,=20forgot/reset=20password,=20verify-email,?= =?UTF-8?q?=202fa,=20oauth=20callback=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/auth/auth-layout.vue | 152 +++++++++++++++ .../src/components/auth/auth-loading.vue | 12 ++ apps/web-vue/src/pages/auth/2fa.vue | 113 +++++++++++- .../src/pages/auth/forgot-password.vue | 109 ++++++++++- apps/web-vue/src/pages/auth/login.vue | 138 +++++++++++++- .../web-vue/src/pages/auth/oauth-callback.vue | 49 ++++- .../web-vue/src/pages/auth/reset-password.vue | 173 +++++++++++++++++- apps/web-vue/src/pages/auth/signup.vue | 160 +++++++++++++++- apps/web-vue/src/pages/auth/verify-email.vue | 92 +++++++++- 9 files changed, 977 insertions(+), 21 deletions(-) create mode 100644 apps/web-vue/src/components/auth/auth-layout.vue create mode 100644 apps/web-vue/src/components/auth/auth-loading.vue diff --git a/apps/web-vue/src/components/auth/auth-layout.vue b/apps/web-vue/src/components/auth/auth-layout.vue new file mode 100644 index 0000000..bd3c3f4 --- /dev/null +++ b/apps/web-vue/src/components/auth/auth-layout.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/apps/web-vue/src/components/auth/auth-loading.vue b/apps/web-vue/src/components/auth/auth-loading.vue new file mode 100644 index 0000000..aa644a1 --- /dev/null +++ b/apps/web-vue/src/components/auth/auth-loading.vue @@ -0,0 +1,12 @@ + + + diff --git a/apps/web-vue/src/pages/auth/2fa.vue b/apps/web-vue/src/pages/auth/2fa.vue index 29142d8..0f84e30 100644 --- a/apps/web-vue/src/pages/auth/2fa.vue +++ b/apps/web-vue/src/pages/auth/2fa.vue @@ -1,6 +1,113 @@ - + diff --git a/apps/web-vue/src/pages/auth/forgot-password.vue b/apps/web-vue/src/pages/auth/forgot-password.vue index 1a93dd7..aa6c25b 100644 --- a/apps/web-vue/src/pages/auth/forgot-password.vue +++ b/apps/web-vue/src/pages/auth/forgot-password.vue @@ -1,6 +1,109 @@ - + diff --git a/apps/web-vue/src/pages/auth/login.vue b/apps/web-vue/src/pages/auth/login.vue index 10ef5e7..c1e5837 100644 --- a/apps/web-vue/src/pages/auth/login.vue +++ b/apps/web-vue/src/pages/auth/login.vue @@ -1,6 +1,138 @@ - + diff --git a/apps/web-vue/src/pages/auth/oauth-callback.vue b/apps/web-vue/src/pages/auth/oauth-callback.vue index 0ab21df..66bf4de 100644 --- a/apps/web-vue/src/pages/auth/oauth-callback.vue +++ b/apps/web-vue/src/pages/auth/oauth-callback.vue @@ -1,6 +1,49 @@ - + diff --git a/apps/web-vue/src/pages/auth/reset-password.vue b/apps/web-vue/src/pages/auth/reset-password.vue index a419274..7413122 100644 --- a/apps/web-vue/src/pages/auth/reset-password.vue +++ b/apps/web-vue/src/pages/auth/reset-password.vue @@ -1,6 +1,173 @@ - + diff --git a/apps/web-vue/src/pages/auth/signup.vue b/apps/web-vue/src/pages/auth/signup.vue index 70cd4c1..ceae79c 100644 --- a/apps/web-vue/src/pages/auth/signup.vue +++ b/apps/web-vue/src/pages/auth/signup.vue @@ -1,6 +1,160 @@ - + diff --git a/apps/web-vue/src/pages/auth/verify-email.vue b/apps/web-vue/src/pages/auth/verify-email.vue index da58dee..0fa6e38 100644 --- a/apps/web-vue/src/pages/auth/verify-email.vue +++ b/apps/web-vue/src/pages/auth/verify-email.vue @@ -1,6 +1,92 @@ - + From b912aa36aef4e5cb28d52c3ca03307a8250a72d1 Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 05:10:04 +0700 Subject: [PATCH 08/31] chore(web-vue): drop unused auth-loading (no Suspense fallback needed) --- apps/web-vue/src/components/auth/auth-loading.vue | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 apps/web-vue/src/components/auth/auth-loading.vue diff --git a/apps/web-vue/src/components/auth/auth-loading.vue b/apps/web-vue/src/components/auth/auth-loading.vue deleted file mode 100644 index aa644a1..0000000 --- a/apps/web-vue/src/components/auth/auth-loading.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - From 40e334d1c1b20eb3b3dbf849aae5a59bd27c838f Mon Sep 17 00:00:00 2001 From: Xiro The Dev Date: Tue, 1 Sep 2026 05:15:38 +0700 Subject: [PATCH 09/31] feat(web-vue): shop, cart, checkout pages + cart drawer (T5, #44) --- apps/web-vue/src/App.vue | 2 + .../components/shop/delete-review-dialog.vue | 49 +++ .../components/shop/floating-cart-button.vue | 228 +++++++++++ .../src/components/shop/product-actions.vue | 74 ++++ .../shop/product-additional-info.vue | 39 ++ .../components/shop/product-description.vue | 22 ++ .../src/components/shop/product-features.vue | 30 ++ .../components/shop/product-image-gallery.vue | 89 +++++ .../src/components/shop/product-info.vue | 71 ++++ .../components/shop/product-page-content.vue | 283 ++++++++++++++ .../shop/product-quantity-selector.vue | 25 ++ .../src/components/shop/product-reviews.vue | 79 ++++ .../components/shop/product-size-guide.vue | 128 +++++++ .../components/shop/product-size-selector.vue | 75 ++++ .../src/components/shop/quantity-selector.vue | 108 ++++++ .../src/components/shop/review-edit-form.vue | 104 +++++ .../src/components/shop/review-form.vue | 152 ++++++++ .../src/components/shop/review-item.vue | 116 ++++++ apps/web-vue/src/pages/cart.vue | 206 +++++++++- apps/web-vue/src/pages/checkout.vue | 361 +++++++++++++++++- apps/web-vue/src/pages/shop/ao-thun.vue | 17 +- apps/web-vue/src/pages/shop/day-deo.vue | 17 +- apps/web-vue/src/pages/shop/index.vue | 20 +- apps/web-vue/src/pages/shop/moc-khoa.vue | 17 +- apps/web-vue/src/pages/shop/pad-chuot.vue | 17 +- 25 files changed, 2308 insertions(+), 21 deletions(-) create mode 100644 apps/web-vue/src/components/shop/delete-review-dialog.vue create mode 100644 apps/web-vue/src/components/shop/floating-cart-button.vue create mode 100644 apps/web-vue/src/components/shop/product-actions.vue create mode 100644 apps/web-vue/src/components/shop/product-additional-info.vue create mode 100644 apps/web-vue/src/components/shop/product-description.vue create mode 100644 apps/web-vue/src/components/shop/product-features.vue create mode 100644 apps/web-vue/src/components/shop/product-image-gallery.vue create mode 100644 apps/web-vue/src/components/shop/product-info.vue create mode 100644 apps/web-vue/src/components/shop/product-page-content.vue create mode 100644 apps/web-vue/src/components/shop/product-quantity-selector.vue create mode 100644 apps/web-vue/src/components/shop/product-reviews.vue create mode 100644 apps/web-vue/src/components/shop/product-size-guide.vue create mode 100644 apps/web-vue/src/components/shop/product-size-selector.vue create mode 100644 apps/web-vue/src/components/shop/quantity-selector.vue create mode 100644 apps/web-vue/src/components/shop/review-edit-form.vue create mode 100644 apps/web-vue/src/components/shop/review-form.vue create mode 100644 apps/web-vue/src/components/shop/review-item.vue diff --git a/apps/web-vue/src/App.vue b/apps/web-vue/src/App.vue index 2232018..5de8eed 100644 --- a/apps/web-vue/src/App.vue +++ b/apps/web-vue/src/App.vue @@ -4,6 +4,7 @@ import { useHead } from '@unhead/vue'; import CsrfInitializer from '@/components/CsrfInitializer.vue'; import Footer from '@/components/Footer.vue'; import Navbar from '@/components/Navbar.vue'; +import FloatingCartButton from '@/components/shop/floating-cart-button.vue'; import { provideAuth } from '@/composables/use-auth'; import { useCartDrawer } from '@/composables/use-cart-drawer'; import { defaultMetadata } from '@/lib/metadata'; @@ -31,5 +32,6 @@ useHead({
+