diff --git a/CONTEXT-MAP.md b/CONTEXT-MAP.md index 0f6cbb81..4b99b870 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/.env.example b/apps/web-vue/.env.example new file mode 100644 index 00000000..9a367415 --- /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/.prettierignore b/apps/web-vue/.prettierignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/apps/web-vue/.prettierignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/apps/web-vue/CONTEXT.md b/apps/web-vue/CONTEXT.md new file mode 100644 index 00000000..e28796cb --- /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 diff --git a/apps/web-vue/index.html b/apps/web-vue/index.html new file mode 100644 index 00000000..ac816e75 --- /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 00000000..d6aabde3 --- /dev/null +++ b/apps/web-vue/package.json @@ -0,0 +1,57 @@ +{ + "name": "web-vue", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "vue-tsc --noEmit", + "test": "bun test --pass-with-no-tests", + "format": "bunx --bun --no-install prettier --write \"**/*.{js,ts,vue,json,css}\"", + "format:check": "bunx --bun --no-install prettier --check \"**/*.{js,ts,vue,json,css}\"" + }, + "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/public/icons/facebook.webp b/apps/web-vue/public/icons/facebook.webp new file mode 100644 index 00000000..783e29a8 Binary files /dev/null and b/apps/web-vue/public/icons/facebook.webp differ diff --git a/apps/web-vue/public/icons/gmail.webp b/apps/web-vue/public/icons/gmail.webp new file mode 100644 index 00000000..c12ae9d5 Binary files /dev/null and b/apps/web-vue/public/icons/gmail.webp differ diff --git a/apps/web-vue/public/icons/messenger.webp b/apps/web-vue/public/icons/messenger.webp new file mode 100644 index 00000000..70edd176 Binary files /dev/null and b/apps/web-vue/public/icons/messenger.webp differ diff --git a/apps/web-vue/public/icons/telephone.webp b/apps/web-vue/public/icons/telephone.webp new file mode 100644 index 00000000..ff971834 Binary files /dev/null and b/apps/web-vue/public/icons/telephone.webp differ diff --git a/apps/web-vue/public/image/ceremony-20-12-2025.webp b/apps/web-vue/public/image/ceremony-20-12-2025.webp new file mode 100644 index 00000000..272e59ac Binary files /dev/null and b/apps/web-vue/public/image/ceremony-20-12-2025.webp differ diff --git a/apps/web-vue/public/image/chunhiem-lamchidinh.webp b/apps/web-vue/public/image/chunhiem-lamchidinh.webp new file mode 100644 index 00000000..4b4bb1b7 Binary files /dev/null and b/apps/web-vue/public/image/chunhiem-lamchidinh.webp differ diff --git a/apps/web-vue/public/image/hero-image.webp b/apps/web-vue/public/image/hero-image.webp new file mode 100644 index 00000000..c2a83b5e Binary files /dev/null and b/apps/web-vue/public/image/hero-image.webp differ diff --git a/apps/web-vue/public/image/khach-hang.webp b/apps/web-vue/public/image/khach-hang.webp new file mode 100644 index 00000000..cc5c0fc8 Binary files /dev/null and b/apps/web-vue/public/image/khach-hang.webp differ diff --git a/apps/web-vue/public/image/ton-chi.webp b/apps/web-vue/public/image/ton-chi.webp new file mode 100644 index 00000000..2f08598d Binary files /dev/null and b/apps/web-vue/public/image/ton-chi.webp differ diff --git a/apps/web-vue/public/image/uit-school.webp b/apps/web-vue/public/image/uit-school.webp new file mode 100644 index 00000000..77848c34 Binary files /dev/null and b/apps/web-vue/public/image/uit-school.webp differ 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 00000000..5b0a59a6 --- /dev/null +++ b/apps/web-vue/public/image/wds-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web-vue/public/shop/ao-thun.webp b/apps/web-vue/public/shop/ao-thun.webp new file mode 100644 index 00000000..76809560 Binary files /dev/null and b/apps/web-vue/public/shop/ao-thun.webp differ diff --git a/apps/web-vue/public/shop/day-deo.webp b/apps/web-vue/public/shop/day-deo.webp new file mode 100644 index 00000000..83271c2b Binary files /dev/null and b/apps/web-vue/public/shop/day-deo.webp differ diff --git a/apps/web-vue/public/shop/moc-khoa.webp b/apps/web-vue/public/shop/moc-khoa.webp new file mode 100644 index 00000000..2c35e398 Binary files /dev/null and b/apps/web-vue/public/shop/moc-khoa.webp differ diff --git a/apps/web-vue/public/shop/pad-chuot.webp b/apps/web-vue/public/shop/pad-chuot.webp new file mode 100644 index 00000000..0805868c Binary files /dev/null and b/apps/web-vue/public/shop/pad-chuot.webp differ diff --git a/apps/web-vue/src/App.vue b/apps/web-vue/src/App.vue new file mode 100644 index 00000000..515f0536 --- /dev/null +++ b/apps/web-vue/src/App.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/web-vue/src/assets/globals.css b/apps/web-vue/src/assets/globals.css new file mode 100644 index 00000000..9ce15cf1 --- /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/components/CsrfInitializer.vue b/apps/web-vue/src/components/CsrfInitializer.vue new file mode 100644 index 00000000..bb6593ce --- /dev/null +++ b/apps/web-vue/src/components/CsrfInitializer.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/web-vue/src/components/FeaturesGrid.vue b/apps/web-vue/src/components/FeaturesGrid.vue new file mode 100644 index 00000000..0efb1488 --- /dev/null +++ b/apps/web-vue/src/components/FeaturesGrid.vue @@ -0,0 +1,251 @@ + + + diff --git a/apps/web-vue/src/components/Footer.vue b/apps/web-vue/src/components/Footer.vue new file mode 100644 index 00000000..11aa4224 --- /dev/null +++ b/apps/web-vue/src/components/Footer.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/web-vue/src/components/Hero.vue b/apps/web-vue/src/components/Hero.vue new file mode 100644 index 00000000..a5edee51 --- /dev/null +++ b/apps/web-vue/src/components/Hero.vue @@ -0,0 +1,154 @@ + + + diff --git a/apps/web-vue/src/components/Navbar.vue b/apps/web-vue/src/components/Navbar.vue new file mode 100644 index 00000000..66f56e8e --- /dev/null +++ b/apps/web-vue/src/components/Navbar.vue @@ -0,0 +1,218 @@ + + + diff --git a/apps/web-vue/src/components/TrustSection.vue b/apps/web-vue/src/components/TrustSection.vue new file mode 100644 index 00000000..d0084bc1 --- /dev/null +++ b/apps/web-vue/src/components/TrustSection.vue @@ -0,0 +1,36 @@ + + + diff --git a/apps/web-vue/src/components/account/account-layout.vue b/apps/web-vue/src/components/account/account-layout.vue new file mode 100644 index 00000000..986d8c04 --- /dev/null +++ b/apps/web-vue/src/components/account/account-layout.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/web-vue/src/components/account/avatar-upload.vue b/apps/web-vue/src/components/account/avatar-upload.vue new file mode 100644 index 00000000..509fdfa0 --- /dev/null +++ b/apps/web-vue/src/components/account/avatar-upload.vue @@ -0,0 +1,111 @@ + + + diff --git a/apps/web-vue/src/components/account/profile-form.vue b/apps/web-vue/src/components/account/profile-form.vue new file mode 100644 index 00000000..753262ca --- /dev/null +++ b/apps/web-vue/src/components/account/profile-form.vue @@ -0,0 +1,152 @@ + + + diff --git a/apps/web-vue/src/components/account/security-settings.vue b/apps/web-vue/src/components/account/security-settings.vue new file mode 100644 index 00000000..05878955 --- /dev/null +++ b/apps/web-vue/src/components/account/security-settings.vue @@ -0,0 +1,140 @@ + + + diff --git a/apps/web-vue/src/components/account/sessions-list.vue b/apps/web-vue/src/components/account/sessions-list.vue new file mode 100644 index 00000000..a17b3756 --- /dev/null +++ b/apps/web-vue/src/components/account/sessions-list.vue @@ -0,0 +1,147 @@ + + + diff --git a/apps/web-vue/src/components/achievements/achievement-card.vue b/apps/web-vue/src/components/achievements/achievement-card.vue new file mode 100644 index 00000000..d7a980bf --- /dev/null +++ b/apps/web-vue/src/components/achievements/achievement-card.vue @@ -0,0 +1,50 @@ + + + diff --git a/apps/web-vue/src/components/achievements/achievement-hero.vue b/apps/web-vue/src/components/achievements/achievement-hero.vue new file mode 100644 index 00000000..faee8890 --- /dev/null +++ b/apps/web-vue/src/components/achievements/achievement-hero.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/web-vue/src/components/achievements/achievement-section.vue b/apps/web-vue/src/components/achievements/achievement-section.vue new file mode 100644 index 00000000..56c34965 --- /dev/null +++ b/apps/web-vue/src/components/achievements/achievement-section.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/web-vue/src/components/achievements/timeline-year.vue b/apps/web-vue/src/components/achievements/timeline-year.vue new file mode 100644 index 00000000..de8e4b8f --- /dev/null +++ b/apps/web-vue/src/components/achievements/timeline-year.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/web-vue/src/components/activities/activity-card.vue b/apps/web-vue/src/components/activities/activity-card.vue new file mode 100644 index 00000000..e3578ac2 --- /dev/null +++ b/apps/web-vue/src/components/activities/activity-card.vue @@ -0,0 +1,88 @@ + + + diff --git a/apps/web-vue/src/components/activities/activity-filters.vue b/apps/web-vue/src/components/activities/activity-filters.vue new file mode 100644 index 00000000..ae10fda2 --- /dev/null +++ b/apps/web-vue/src/components/activities/activity-filters.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/web-vue/src/components/activities/activity-hero.vue b/apps/web-vue/src/components/activities/activity-hero.vue new file mode 100644 index 00000000..60e3860b --- /dev/null +++ b/apps/web-vue/src/components/activities/activity-hero.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/web-vue/src/components/activities/filter-button.vue b/apps/web-vue/src/components/activities/filter-button.vue new file mode 100644 index 00000000..bdd65319 --- /dev/null +++ b/apps/web-vue/src/components/activities/filter-button.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/web-vue/src/components/activities/newsletter-cta.vue b/apps/web-vue/src/components/activities/newsletter-cta.vue new file mode 100644 index 00000000..6d1503e0 --- /dev/null +++ b/apps/web-vue/src/components/activities/newsletter-cta.vue @@ -0,0 +1,50 @@ + + + diff --git a/apps/web-vue/src/components/admin/admin-chart.vue b/apps/web-vue/src/components/admin/admin-chart.vue new file mode 100644 index 00000000..50a47747 --- /dev/null +++ b/apps/web-vue/src/components/admin/admin-chart.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/apps/web-vue/src/components/admin/admin-data-table.vue b/apps/web-vue/src/components/admin/admin-data-table.vue new file mode 100644 index 00000000..226de73b --- /dev/null +++ b/apps/web-vue/src/components/admin/admin-data-table.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/web-vue/src/components/admin/admin-header.vue b/apps/web-vue/src/components/admin/admin-header.vue new file mode 100644 index 00000000..2060e8a4 --- /dev/null +++ b/apps/web-vue/src/components/admin/admin-header.vue @@ -0,0 +1,47 @@ + + + diff --git a/apps/web-vue/src/components/admin/admin-layout.vue b/apps/web-vue/src/components/admin/admin-layout.vue new file mode 100644 index 00000000..0e0b0226 --- /dev/null +++ b/apps/web-vue/src/components/admin/admin-layout.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/web-vue/src/components/admin/admin-sidebar.vue b/apps/web-vue/src/components/admin/admin-sidebar.vue new file mode 100644 index 00000000..d4e01136 --- /dev/null +++ b/apps/web-vue/src/components/admin/admin-sidebar.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/web-vue/src/components/admin/blog-editor.vue b/apps/web-vue/src/components/admin/blog-editor.vue new file mode 100644 index 00000000..d107fbe0 --- /dev/null +++ b/apps/web-vue/src/components/admin/blog-editor.vue @@ -0,0 +1,310 @@ + + + diff --git a/apps/web-vue/src/components/admin/column-visibility-toggle.vue b/apps/web-vue/src/components/admin/column-visibility-toggle.vue new file mode 100644 index 00000000..3975e9b0 --- /dev/null +++ b/apps/web-vue/src/components/admin/column-visibility-toggle.vue @@ -0,0 +1,59 @@ + + + diff --git a/apps/web-vue/src/components/admin/data-table.vue b/apps/web-vue/src/components/admin/data-table.vue new file mode 100644 index 00000000..6c8878d8 --- /dev/null +++ b/apps/web-vue/src/components/admin/data-table.vue @@ -0,0 +1,70 @@ + + + diff --git a/apps/web-vue/src/components/admin/pagination.test.ts b/apps/web-vue/src/components/admin/pagination.test.ts new file mode 100644 index 00000000..68d2b20c --- /dev/null +++ b/apps/web-vue/src/components/admin/pagination.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'bun:test'; + +import { nextDisabledFor } from './pagination'; + +const rowsOf = (n: number) => Array.from({ length: n }); + +test('total known: next enabled while more pages exist', () => { + expect(nextDisabledFor(1, 25, rowsOf(10), 10)).toBe(false); + expect(nextDisabledFor(2, 25, rowsOf(10), 10)).toBe(false); +}); + +test('total known: next disabled on and past the last page', () => { + expect(nextDisabledFor(3, 25, rowsOf(5), 10)).toBe(true); + expect(nextDisabledFor(2, 20, rowsOf(10), 10)).toBe(true); // exact multiple + expect(nextDisabledFor(1, 0, rowsOf(0), 10)).toBe(true); // empty list +}); + +test('total unknown: short page disables next, full page enables it', () => { + expect(nextDisabledFor(1, undefined, rowsOf(10), 10)).toBe(false); + expect(nextDisabledFor(1, undefined, rowsOf(4), 10)).toBe(true); + expect(nextDisabledFor(1, undefined, rowsOf(0), 10)).toBe(true); +}); diff --git a/apps/web-vue/src/components/admin/pagination.ts b/apps/web-vue/src/components/admin/pagination.ts new file mode 100644 index 00000000..f048fb1a --- /dev/null +++ b/apps/web-vue/src/components/admin/pagination.ts @@ -0,0 +1,11 @@ +// Next-page disable rule for admin tables: with a known total trust it; without one +// (API omits it), a short page means the last page. +export function nextDisabledFor( + page: number, + total: number | undefined, + rows: readonly unknown[], + limit: number, +): boolean { + if (total != null) return page * limit >= total; + return rows.length < limit; +} diff --git a/apps/web-vue/src/components/admin/product-editor.vue b/apps/web-vue/src/components/admin/product-editor.vue new file mode 100644 index 00000000..b5b630a3 --- /dev/null +++ b/apps/web-vue/src/components/admin/product-editor.vue @@ -0,0 +1,196 @@ + + + diff --git a/apps/web-vue/src/components/admin/table-actions.vue b/apps/web-vue/src/components/admin/table-actions.vue new file mode 100644 index 00000000..6939c5c8 --- /dev/null +++ b/apps/web-vue/src/components/admin/table-actions.vue @@ -0,0 +1,76 @@ + + + diff --git a/apps/web-vue/src/components/admin/table-filters.vue b/apps/web-vue/src/components/admin/table-filters.vue new file mode 100644 index 00000000..00ce0a5a --- /dev/null +++ b/apps/web-vue/src/components/admin/table-filters.vue @@ -0,0 +1,90 @@ + + + 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 00000000..bd3c3f40 --- /dev/null +++ b/apps/web-vue/src/components/auth/auth-layout.vue @@ -0,0 +1,152 @@ + + + + + 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 00000000..6fd608bc --- /dev/null +++ b/apps/web-vue/src/components/blog/blog-post-card.vue @@ -0,0 +1,43 @@ + + + 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 00000000..0bccb2d7 --- /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 00000000..3c4d4d67 --- /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/calendar/CalendarContainer.vue b/apps/web-vue/src/components/calendar/CalendarContainer.vue new file mode 100644 index 00000000..083ff5d9 --- /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 00000000..570c3ad1 --- /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 00000000..c1c46327 --- /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 00000000..1c71371b --- /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 00000000..cd675ffa --- /dev/null +++ b/apps/web-vue/src/components/calendar/event-helpers.ts @@ -0,0 +1,70 @@ +import { relativeTime } from '@/lib/date'; +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)}`; +} + +export function getRelativeTime(event: Event): string { + const now = Date.now(); + if (event.start.getTime() > now) return `Bắt đầu ${relativeTime(event.start)}`; + if (event.end.getTime() > now) return `Đang diễn ra - Kết thúc ${relativeTime(event.end)}`; + return `Đã kết thúc ${relativeTime(event.end)}`; +} diff --git a/apps/web-vue/src/components/generation/desktop-bento-grid.vue b/apps/web-vue/src/components/generation/desktop-bento-grid.vue new file mode 100644 index 00000000..69d340b5 --- /dev/null +++ b/apps/web-vue/src/components/generation/desktop-bento-grid.vue @@ -0,0 +1,135 @@ + + + diff --git a/apps/web-vue/src/components/generation/expandable-generation-section.vue b/apps/web-vue/src/components/generation/expandable-generation-section.vue new file mode 100644 index 00000000..e9929049 --- /dev/null +++ b/apps/web-vue/src/components/generation/expandable-generation-section.vue @@ -0,0 +1,130 @@ + + + diff --git a/apps/web-vue/src/components/generation/generation-member-avatar.vue b/apps/web-vue/src/components/generation/generation-member-avatar.vue new file mode 100644 index 00000000..ffbbec0c --- /dev/null +++ b/apps/web-vue/src/components/generation/generation-member-avatar.vue @@ -0,0 +1,103 @@ + + + diff --git a/apps/web-vue/src/components/glass-card.vue b/apps/web-vue/src/components/glass-card.vue new file mode 100644 index 00000000..a247a997 --- /dev/null +++ b/apps/web-vue/src/components/glass-card.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/web-vue/src/components/legal/legal-layout.vue b/apps/web-vue/src/components/legal/legal-layout.vue new file mode 100644 index 00000000..eb60eabb --- /dev/null +++ b/apps/web-vue/src/components/legal/legal-layout.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/apps/web-vue/src/components/orders/order-card.vue b/apps/web-vue/src/components/orders/order-card.vue new file mode 100644 index 00000000..d961eff4 --- /dev/null +++ b/apps/web-vue/src/components/orders/order-card.vue @@ -0,0 +1,116 @@ + + + diff --git a/apps/web-vue/src/components/partner/partner-card.vue b/apps/web-vue/src/components/partner/partner-card.vue new file mode 100644 index 00000000..bcc1afde --- /dev/null +++ b/apps/web-vue/src/components/partner/partner-card.vue @@ -0,0 +1,61 @@ + + + diff --git a/apps/web-vue/src/components/partner/partner-hero.vue b/apps/web-vue/src/components/partner/partner-hero.vue new file mode 100644 index 00000000..b206c743 --- /dev/null +++ b/apps/web-vue/src/components/partner/partner-hero.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/web-vue/src/components/partner/partner-section.vue b/apps/web-vue/src/components/partner/partner-section.vue new file mode 100644 index 00000000..37b37dd0 --- /dev/null +++ b/apps/web-vue/src/components/partner/partner-section.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/web-vue/src/components/shop/delete-review-dialog.vue b/apps/web-vue/src/components/shop/delete-review-dialog.vue new file mode 100644 index 00000000..1fdaf377 --- /dev/null +++ b/apps/web-vue/src/components/shop/delete-review-dialog.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/web-vue/src/components/shop/floating-cart-button.vue b/apps/web-vue/src/components/shop/floating-cart-button.vue new file mode 100644 index 00000000..a05eabc5 --- /dev/null +++ b/apps/web-vue/src/components/shop/floating-cart-button.vue @@ -0,0 +1,206 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-actions.vue b/apps/web-vue/src/components/shop/product-actions.vue new file mode 100644 index 00000000..b0a3026c --- /dev/null +++ b/apps/web-vue/src/components/shop/product-actions.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-additional-info.vue b/apps/web-vue/src/components/shop/product-additional-info.vue new file mode 100644 index 00000000..b9b02dfb --- /dev/null +++ b/apps/web-vue/src/components/shop/product-additional-info.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-description.vue b/apps/web-vue/src/components/shop/product-description.vue new file mode 100644 index 00000000..c491ce71 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-description.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-features.vue b/apps/web-vue/src/components/shop/product-features.vue new file mode 100644 index 00000000..2c715643 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-features.vue @@ -0,0 +1,30 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-image-gallery.vue b/apps/web-vue/src/components/shop/product-image-gallery.vue new file mode 100644 index 00000000..f413bb7d --- /dev/null +++ b/apps/web-vue/src/components/shop/product-image-gallery.vue @@ -0,0 +1,89 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-info.vue b/apps/web-vue/src/components/shop/product-info.vue new file mode 100644 index 00000000..aeb4310b --- /dev/null +++ b/apps/web-vue/src/components/shop/product-info.vue @@ -0,0 +1,71 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-page-content.vue b/apps/web-vue/src/components/shop/product-page-content.vue new file mode 100644 index 00000000..01b2b252 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-page-content.vue @@ -0,0 +1,278 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-quantity-selector.vue b/apps/web-vue/src/components/shop/product-quantity-selector.vue new file mode 100644 index 00000000..7c3b42f5 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-quantity-selector.vue @@ -0,0 +1,25 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-reviews.vue b/apps/web-vue/src/components/shop/product-reviews.vue new file mode 100644 index 00000000..47d2e343 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-reviews.vue @@ -0,0 +1,79 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-size-guide.vue b/apps/web-vue/src/components/shop/product-size-guide.vue new file mode 100644 index 00000000..1d54bb9a --- /dev/null +++ b/apps/web-vue/src/components/shop/product-size-guide.vue @@ -0,0 +1,128 @@ + + + diff --git a/apps/web-vue/src/components/shop/product-size-selector.vue b/apps/web-vue/src/components/shop/product-size-selector.vue new file mode 100644 index 00000000..e361bf07 --- /dev/null +++ b/apps/web-vue/src/components/shop/product-size-selector.vue @@ -0,0 +1,75 @@ + + + diff --git a/apps/web-vue/src/components/shop/quantity-selector.vue b/apps/web-vue/src/components/shop/quantity-selector.vue new file mode 100644 index 00000000..8d3be643 --- /dev/null +++ b/apps/web-vue/src/components/shop/quantity-selector.vue @@ -0,0 +1,108 @@ + + + diff --git a/apps/web-vue/src/components/shop/review-edit-form.vue b/apps/web-vue/src/components/shop/review-edit-form.vue new file mode 100644 index 00000000..541625e4 --- /dev/null +++ b/apps/web-vue/src/components/shop/review-edit-form.vue @@ -0,0 +1,104 @@ + + +