Compare commits
23 Commits
develop-go
...
develop-gr
| Author | SHA1 | Date | |
|---|---|---|---|
| b60f65e4d1 | |||
| e854c68ad0 | |||
| b787cd161a | |||
| bd8b21955e | |||
| 87c99e64cd | |||
| baa8811e9e | |||
| fa88fe26b3 | |||
| 90d8409aa9 | |||
| b4bbacd9f1 | |||
| 8b85736903 | |||
| 3beabcfe7f | |||
| 35117b7be9 | |||
| e3587eff71 | |||
| 57903b80b6 | |||
| 5c0ca0e139 | |||
| 9276603a70 | |||
| dc06412f79 | |||
| edc1a33547 | |||
| 3c24da4af8 | |||
| 3491a0a08e | |||
| 6d04f1cbdc | |||
| bbe15d5f3e | |||
| dba9713d96 |
@@ -3,7 +3,16 @@
|
|||||||
"allow": [
|
"allow": [
|
||||||
"Bash(bun run build)",
|
"Bash(bun run build)",
|
||||||
"mcp__ide__getDiagnostics",
|
"mcp__ide__getDiagnostics",
|
||||||
"Bash(bun install:*)"
|
"Bash(bun install:*)",
|
||||||
|
"Bash(bun preview:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(python -:*)",
|
||||||
|
"Bash(bun run:*)",
|
||||||
|
"Bash(bunx:*)",
|
||||||
|
"Bash(bun:*)",
|
||||||
|
"Bash(git diff:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git status:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
68
.dockerignore
Normal file
68
.dockerignore
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.rsbuild
|
||||||
|
node_modules
|
||||||
|
/node_modules
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
coverage
|
||||||
|
.coverage
|
||||||
|
.nyc_output
|
||||||
|
test
|
||||||
|
tests
|
||||||
|
__tests__
|
||||||
|
*.test.js
|
||||||
|
*.test.ts
|
||||||
|
*.spec.js
|
||||||
|
*.spec.ts
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
.eslintrc*
|
||||||
|
.prettierrc*
|
||||||
|
.stylelintrc*
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.husky
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
This file provides guidance for AI coding agents working with the Holistream codebase.
|
This file provides guidance for AI coding agents working with the Holistream codebase.
|
||||||
|
hallo
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities for content creators.
|
**Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities for content creators.
|
||||||
|
|||||||
219
CLAUDE.md
219
CLAUDE.md
@@ -2,197 +2,82 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
## Project overview
|
||||||
|
|
||||||
Holistream is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities.
|
`stream-ui` is a Vue 3 SSR frontend deployed on Cloudflare Workers. It uses Hono as the Worker server layer and a custom Vite SSR setup rather than Nuxt.
|
||||||
|
|
||||||
## Technology Stack
|
## Common commands
|
||||||
|
|
||||||
- **Framework**: Vue 3 with JSX/TSX support
|
Run all commands from `stream-ui/`.
|
||||||
- **Router**: Vue Router 5 with SSR-aware history
|
|
||||||
- **Server**: Hono framework on Cloudflare Workers
|
|
||||||
- **Build Tool**: Vite 7 with custom SSR plugin
|
|
||||||
- **Styling**: UnoCSS (Tailwind-like utility-first CSS)
|
|
||||||
- **UI Components**: PrimeVue 4 with Aura theme
|
|
||||||
- **State Management**: Pinia + Pinia Colada for server state
|
|
||||||
- **HTTP Client**: Auto-generated from OpenAPI spec via swagger-typescript-api
|
|
||||||
- **Package Manager**: Bun
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development server with hot reload
|
# Install dependencies
|
||||||
bun dev
|
bun install
|
||||||
|
|
||||||
# Production build (client + worker)
|
# Start local dev server
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# Build client + worker bundles
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Preview production build locally
|
# Preview production build locally
|
||||||
bun preview
|
bun run preview
|
||||||
|
|
||||||
# Deploy to Cloudflare Workers
|
# Deploy to Cloudflare Workers
|
||||||
bun run deploy
|
bun run deploy
|
||||||
|
|
||||||
# Generate TypeScript types from Wrangler config
|
# Regenerate Cloudflare binding types from Wrangler config
|
||||||
bun run cf-typegen
|
bun run cf-typegen
|
||||||
|
|
||||||
# View Cloudflare Worker logs
|
# Tail Cloudflare Worker logs
|
||||||
bun run tail
|
bun run tail
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: The project uses Bun as the package manager. If using npm/yarn, replace `bun` with `npm run` or `yarn`.
|
Notes:
|
||||||
|
- This project uses Bun (`bun.lock` is present).
|
||||||
|
- There is currently no configured `test` script.
|
||||||
|
- There is currently no configured `lint` script.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### SSR Architecture
|
### SSR entrypoints
|
||||||
|
- `src/index.tsx`: Hono Worker entry; registers middleware, proxy routes, merge/display/manifest routes, then SSR routes
|
||||||
|
- `src/main.ts`: shared app factory for SSR and client hydration
|
||||||
|
- `src/client.ts`: client-side hydration entry
|
||||||
|
- `ssrPlugin.ts`: custom Vite SSR plugin that builds the client first, injects the Vite manifest, and swaps environment-specific modules
|
||||||
|
|
||||||
The app uses a custom SSR setup (`ssrPlugin.ts`) that:
|
### Routing and app structure
|
||||||
- Builds the client bundle FIRST, then the Worker bundle
|
- Routes live in `src/routes/index.ts`.
|
||||||
- Injects the Vite manifest into the server build for asset rendering
|
- Routing is SSR-aware: `createMemoryHistory()` on the server and `createWebHistory()` in the browser.
|
||||||
- Uses environment-based module resolution for `httpClientAdapter` and `liteMqtt`
|
- The app is split into:
|
||||||
|
- public pages
|
||||||
|
- auth pages
|
||||||
|
- protected dashboard/settings pages
|
||||||
|
- Current protected areas include `videos`, `notification`, and `settings/*` routes.
|
||||||
|
|
||||||
Entry points:
|
### State and hydration
|
||||||
- **Server**: `src/index.tsx` - Hono app that renders Vue SSR stream
|
- Pinia is used for app state.
|
||||||
- **Client**: `src/client.ts` - Hydrates the SSR-rendered app
|
- `@pinia/colada` is used for server-state/query hydration.
|
||||||
|
- SSR serializes Pinia state into `$p` and query cache into `$colada`; `src/client.ts` restores both during hydration.
|
||||||
|
- `src/stores/auth.ts` owns session state and route guards depend on `auth.user`.
|
||||||
|
|
||||||
### Module Aliases
|
### API integration
|
||||||
|
- `src/api/client.ts` is generated by `swagger-typescript-api`; do not hand-edit generated sections.
|
||||||
|
- API access should go through the generated client and `@httpClientAdapter`, not raw `fetch`.
|
||||||
|
- `src/api/httpClientAdapter.server.ts` handles SSR-side API calls by forwarding request headers/cookies and proxying frontend `/r/*` requests to `https://api.pipic.fun`.
|
||||||
|
- `src/api/httpClientAdapter.client.ts` is the browser-side adapter.
|
||||||
|
|
||||||
- `@/` → `src/`
|
### Notable flows
|
||||||
- `@httpClientAdapter` → `src/api/httpClientAdapter.server.ts` (SSR) or `.client.ts` (browser)
|
- `src/stores/auth.ts` initializes the logged-in user from `/me` and opens an MQTT connection after login.
|
||||||
- `@liteMqtt` → `src/lib/liteMqtt.server.ts` (SSR) or `.ts` (browser)
|
- `src/composables/useUploadQueue.ts` implements the custom upload queue:
|
||||||
|
- 90MB chunks
|
||||||
|
- max 3 parallel uploads
|
||||||
|
- max 3 retries
|
||||||
|
- max 5 queued items
|
||||||
|
- Styling uses UnoCSS (`uno.config.ts`).
|
||||||
|
|
||||||
### State Management Pattern
|
## Important notes
|
||||||
|
|
||||||
Uses **Pinia Colada** for server state with SSR hydration:
|
- Prefer the actual current code over older documentation when they conflict.
|
||||||
- Queries are fetched server-side and serialized to `window.__APP_DATA__`
|
- The previous version of this file contained stale route and dependency details; verify against `src/routes/index.ts` and `package.json` before assuming old pages or libraries still exist.
|
||||||
- Client hydrates the query cache on startup via `hydrateQueryCache()`
|
- Any frontend change that affects API contracts should be checked against the backend repo (`../stream.api`) as well.
|
||||||
- Pinia state is similarly serialized and restored via `PiniaSharedState` plugin
|
|
||||||
|
|
||||||
### API Client Architecture
|
|
||||||
|
|
||||||
The API client (`src/api/client.ts`) is auto-generated from OpenAPI spec:
|
|
||||||
- Uses `customFetch` adapter that differs between client/server
|
|
||||||
- Server adapter (`httpClientAdapter.server.ts`): Forwards cookies via `hono/context-storage`, merges headers, calls `https://api.pipic.fun`
|
|
||||||
- Client adapter (`httpClientAdapter.client.ts`): Standard fetch with `credentials: "include"`
|
|
||||||
- API proxy route: `/r/*` paths proxy to `https://api.pipic.fun` via `apiProxyMiddleware`
|
|
||||||
- Base API URL constant: `baseAPIURL = "https://api.pipic.fun"`
|
|
||||||
|
|
||||||
### Routing Structure
|
|
||||||
|
|
||||||
Routes are defined in `src/routes/index.ts` with three main layouts:
|
|
||||||
1. **Public** (`/`): Landing page, terms, privacy
|
|
||||||
2. **Auth** (`/login`, `/sign-up`, `/forgot`): Authentication pages (redirects if logged in)
|
|
||||||
3. **Dashboard**: Protected routes requiring auth
|
|
||||||
- `/overview` - Main dashboard
|
|
||||||
- `/upload` - Video upload
|
|
||||||
- `/video` - Video list
|
|
||||||
- `/video/:id` - Video detail/edit
|
|
||||||
- `/payments-and-plans` - Billing
|
|
||||||
- `/notification`, `/profile` - User settings
|
|
||||||
|
|
||||||
Route meta supports `@unhead/vue` for SEO: `meta: { head: { title, meta: [...] } }`
|
|
||||||
|
|
||||||
### Styling System (UnoCSS)
|
|
||||||
|
|
||||||
Configuration in `uno.config.ts`:
|
|
||||||
- **Presets**: Wind4 (Tailwind), Typography, Attributify, Bootstrap buttons
|
|
||||||
- **Custom colors**: `primary` (#14a74b), `accent`, `secondary` (#fd7906), `success`, `info`, `warning`, `danger`
|
|
||||||
- **Shortcuts**: `press-animated` for button press effects
|
|
||||||
- **Transformers**: `transformerCompileClass` (prefix: `_`), `transformerVariantGroup`
|
|
||||||
|
|
||||||
Use `cn()` from `src/lib/utils.ts` for conditional class merging (clsx + tailwind-merge).
|
|
||||||
|
|
||||||
### Component Auto-Import
|
|
||||||
|
|
||||||
Components in `src/components/` are auto-imported via `unplugin-vue-components`:
|
|
||||||
- PrimeVue components resolved via `PrimeVueResolver`
|
|
||||||
- Vue/Pinia/Vue Router APIs auto-imported via `unplugin-auto-import`
|
|
||||||
|
|
||||||
### Auth Flow
|
|
||||||
|
|
||||||
- `useAuthStore` manages auth state with cookie-based sessions
|
|
||||||
- `init()` called on every request to fetch current user via `/me` endpoint
|
|
||||||
- `beforeEach` router guard redirects unauthenticated users from protected routes
|
|
||||||
- MQTT client connects on user login for real-time notifications
|
|
||||||
|
|
||||||
### File Upload Architecture
|
|
||||||
|
|
||||||
Upload queue (`src/composables/useUploadQueue.ts`):
|
|
||||||
- Supports both local files and remote URLs
|
|
||||||
- Presigned POST URLs fetched from API
|
|
||||||
- Parallel chunk upload for large files
|
|
||||||
- Progress tracking with speed calculation
|
|
||||||
- **Chunk configuration**: 90MB chunks, max 3 parallel uploads, max 3 retries
|
|
||||||
- **Upload limits**: Max 5 items in queue
|
|
||||||
- Uses `tmpfiles.org` API for chunk uploads, `/merge` endpoint for finalizing
|
|
||||||
- Cancel support via XHR abort tracking
|
|
||||||
|
|
||||||
### Type Safety
|
|
||||||
|
|
||||||
- TypeScript strict mode enabled
|
|
||||||
- `CloudflareBindings` interface for environment variables (generated via `cf-typegen`)
|
|
||||||
- API types auto-generated from backend OpenAPI spec
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Cloudflare Worker bindings (configured in `wrangler.jsonc`):
|
|
||||||
- No explicit secrets in code - use Wrangler secrets management
|
|
||||||
- `compatibility_date`: "2025-08-03"
|
|
||||||
- `compatibility_flags`: ["nodejs_compat"]
|
|
||||||
|
|
||||||
## Important File Locations
|
|
||||||
|
|
||||||
| Purpose | Path |
|
|
||||||
|---------|------|
|
|
||||||
| Server entry | `src/index.tsx` |
|
|
||||||
| Client entry | `src/client.ts` |
|
|
||||||
| App factory | `src/main.ts` |
|
|
||||||
| Router config | `src/routes/index.ts` |
|
|
||||||
| API client | `src/api/client.ts` |
|
|
||||||
| Auth store | `src/stores/auth.ts` |
|
|
||||||
| SSR plugin | `ssrPlugin.ts` |
|
|
||||||
| UnoCSS config | `uno.config.ts` |
|
|
||||||
| Wrangler config | `wrangler.jsonc` |
|
|
||||||
| Vite config | `vite.config.ts` |
|
|
||||||
|
|
||||||
## Server Structure
|
|
||||||
|
|
||||||
Middleware and routes are organized in `src/server/`:
|
|
||||||
|
|
||||||
**Middlewares** (`src/server/middlewares/`):
|
|
||||||
- `setup.ts` - Global middleware: `contextStorage`, CORS, mobile detection via `is-mobile`
|
|
||||||
- `apiProxy.ts` - Proxies `/r/*` requests to external API
|
|
||||||
|
|
||||||
**Routes** (`src/server/routes/`):
|
|
||||||
- `ssr.ts` - Handles SSR rendering and state serialization
|
|
||||||
- `display.ts`, `merge.ts`, `manifest.ts`, `wellKnown.ts` - API endpoints
|
|
||||||
|
|
||||||
## Development Notes
|
|
||||||
|
|
||||||
- Always use `customFetch` from `@httpClientAdapter` for API calls, never raw fetch
|
|
||||||
- The `honoContext` is provided to Vue app for accessing request context in components
|
|
||||||
- MQTT client in `src/lib/liteMqtt.ts` (using `TinyMqttClient`) handles real-time notifications
|
|
||||||
- Icons are custom Vue components in `src/components/icons/`
|
|
||||||
- Upload indicator is a global component showing queue status
|
|
||||||
- Root component uses error boundary wrapper: `withErrorBoundary(RouterView)` in `src/main.ts`
|
|
||||||
- **Testing & Linting**: There are currently no automated test suites (like Vitest) or linting tools (like ESLint/Prettier) configured.
|
|
||||||
|
|
||||||
## Code Organization
|
|
||||||
|
|
||||||
### Component Structure
|
|
||||||
|
|
||||||
- Keep view components small and focused - extract logical sections into child components
|
|
||||||
- Page views should compose child components, not contain all logic inline
|
|
||||||
- Example: `src/routes/settings/Settings.vue` uses child components in `src/routes/settings/components/`
|
|
||||||
- Components that exceed ~200 lines should be considered for refactoring
|
|
||||||
- Use `components/` subfolder pattern for page-specific components: `src/routes/{feature}/components/`
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
|
|
||||||
- **Use custom SVG icon components** from `src/components/icons/` for UI icons (e.g., `Home`, `Video`, `Bell`, `SettingsIcon`)
|
|
||||||
- Custom icons are Vue components with `filled` prop for active/filled state
|
|
||||||
- PrimeIcons (`pi pi-*` class) should **only** be used for:
|
|
||||||
- Button icons in PrimeVue components (e.g., `icon="pi pi-check"`)
|
|
||||||
- Dialog/action icons where no custom SVG exists
|
|
||||||
- **Do NOT use** `<i class="pi pi-*">` for navigation icons, action buttons, or UI elements that have custom SVG equivalents
|
|
||||||
- When adding new icons, create SVG components in `src/components/icons/` following the existing pattern (support `filled` prop)
|
|
||||||
|
|||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# ---------- Builder stage ----------
|
||||||
|
FROM oven/bun:1.3.10-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy lockfiles & package.json
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY bun.lockb* ./
|
||||||
|
COPY yarn.lock* ./
|
||||||
|
COPY pnpm-lock.yaml* ./
|
||||||
|
|
||||||
|
# Install dependencies (cached)
|
||||||
|
RUN --mount=type=cache,target=/root/.bun bun install
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build app (RSBuild output -> dist)
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Production stage ----------
|
||||||
|
FROM oven/bun:1.3.10-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built files
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Optional health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:3000/ || exit 1
|
||||||
|
|
||||||
|
# Run Bun with fallback install (auto resolves missing deps)
|
||||||
|
CMD [ "bun", "--bun", "dist" ]
|
||||||
19
buf.gen.yaml
Normal file
19
buf.gen.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: v2
|
||||||
|
plugins:
|
||||||
|
# - remote: buf.build/protocolbuffers/go
|
||||||
|
# out: internal/gen/proto
|
||||||
|
# opt:
|
||||||
|
# - paths=source_relative
|
||||||
|
# - remote: buf.build/grpc/go
|
||||||
|
# out: internal/gen/proto
|
||||||
|
# opt:
|
||||||
|
# - paths=source_relative
|
||||||
|
- remote: buf.build/community/stephenh-ts-proto
|
||||||
|
out: ./src/server/utils/proto
|
||||||
|
opt:
|
||||||
|
- env=node
|
||||||
|
- esModuleInterop=true
|
||||||
|
- outputServices=grpc-js
|
||||||
|
- useOptionals=all
|
||||||
|
- forceLong=number
|
||||||
|
- useDate=string
|
||||||
9
buf.yaml
Normal file
9
buf.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: proto
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
520
bun.lock
520
bun.lock
@@ -5,30 +5,42 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "holistream",
|
"name": "holistream",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pinia/colada": "^0.21.2",
|
"@bufbuild/protobuf": "^2.11.0",
|
||||||
"@unhead/vue": "^2.1.2",
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
"@vueuse/core": "^14.2.0",
|
"@hattip/adapter-node": "^0.0.49",
|
||||||
|
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||||
|
"@hiogawa/utils": "^1.7.0",
|
||||||
|
"@hono/node-server": "^1.19.11",
|
||||||
|
"@hono/zod-validator": "^0.7.6",
|
||||||
|
"@pinia/colada": "^1.0.0",
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
|
"@unhead/vue": "^2.1.12",
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"aws4fetch": "^1.0.20",
|
"aws4fetch": "^1.0.20",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"hono": "^4.11.7",
|
"hono": "^4.12.7",
|
||||||
|
"i18next": "^25.8.18",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"i18next-vue": "^5.4.0",
|
||||||
"is-mobile": "^5.0.0",
|
"is-mobile": "^5.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"tailwind-merge": "^3.4.0",
|
"superjson": "^2.2.6",
|
||||||
"vue": "^3.5.27",
|
"tailwind-merge": "^3.5.0",
|
||||||
"vue-router": "^5.0.2",
|
"tweetnacl": "^1.0.3",
|
||||||
|
"vue": "^3.5.30",
|
||||||
|
"vue-router": "^5.0.3",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.23.0",
|
"@types/bun": "^1.3.10",
|
||||||
"@types/node": "^25.2.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||||
"unocss": "^66.6.0",
|
"estree-walker": "2.0.2",
|
||||||
|
"unocss": "^66.6.6",
|
||||||
"unplugin-auto-import": "^21.0.0",
|
"unplugin-auto-import": "^21.0.0",
|
||||||
"unplugin-vue-components": "^31.0.0",
|
"unplugin-vue-components": "^31.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0-beta.16",
|
||||||
"vite-ssr-components": "^0.5.2",
|
"vite-ssr-components": "^0.5.2",
|
||||||
"wrangler": "^4.62.0",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -81,32 +93,22 @@
|
|||||||
|
|
||||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
|
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
|
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="],
|
||||||
|
|
||||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.14.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260218.0" }, "optionalPeers": ["workerd"] }, "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg=="],
|
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.25.5", "", { "dependencies": { "@cloudflare/unenv-preset": "2.14.0", "miniflare": "4.20260302.0", "unenv": "2.0.0-rc.24", "wrangler": "4.68.1", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-dWnJtp/4/m2XQ5Ssnxrh6rb+Jvlkd9pTZhX8MS5sNhdzoULB6vzPkdKaKnaLnYC97iL3j1I2m0gIr15QznKRjA=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260302.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cGtxPByeVrgoqxbmd8qs631wuGwf8yTm/FY44dEW4HdoXrb5jhlE4oWYHFafedkQCvGjY1Vbs3puAiKnuMxTXQ=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260302.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WRGqV6RNXM3xoQblJJw1EHKwx9exyhB18cdnToSCUFPObFhk3fzMLoQh7S+nUHUpto6aUrXPVj6R/4G3UPjCxw=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260302.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gG423mtUjrmlQT+W2+KisLc6qcGcBLR+QcK5x1gje3bu/dF3oNiYuqY7o58A+sQk6IB849UC4UyNclo1RhP2xw=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260302.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7M25noGI4WlSBOhrIaY8xZrnn87OQKtJg9YWAO2EFqGjF1Su5QXGaLlQVF4fAKbqTywbHnI8BAuIsIlUSNkhCg=="],
|
|
||||||
|
|
||||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260302.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jK1L3ADkiWxFzlqZTq2iHW1Bd2Nzu1fmMWCGZw4sMZ2W1B2WCm2wHwO2SX/py4BgylyEN3wuF+5zagbkNKht9A=="],
|
|
||||||
|
|
||||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||||
@@ -159,60 +161,32 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||||
|
|
||||||
|
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
|
||||||
|
|
||||||
|
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
|
||||||
|
|
||||||
|
"@hattip/adapter-node": ["@hattip/adapter-node@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49", "@hattip/polyfills": "0.0.49", "@hattip/walk": "0.0.49" } }, "sha512-BE+Y8Q4U0YcH34FZUYU4DssGKOaZLbNL0zK57Z41UZp0m9kS79ZIolBmjjpPhTVpIlRY3Rs+uhXbVXKk7mUcJA=="],
|
||||||
|
|
||||||
|
"@hattip/core": ["@hattip/core@0.0.49", "", {}, "sha512-3/ZJtC17cv8m6Sph8+nw4exUp9yhEf2Shi7HK6AHSUSBtaaQXZ9rJBVxTfZj3PGNOR/P49UBXOym/52WYKFTJQ=="],
|
||||||
|
|
||||||
|
"@hattip/headers": ["@hattip/headers@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49" } }, "sha512-rrB2lEhTf0+MNVt5WdW184Ky706F1Ze9Aazn/R8c+/FMUYF9yjem2CgXp49csPt3dALsecrnAUOHFiV0LrrHXA=="],
|
||||||
|
|
||||||
|
"@hattip/polyfills": ["@hattip/polyfills@0.0.49", "", { "dependencies": { "@hattip/core": "0.0.49", "@whatwg-node/fetch": "^0.9.22", "node-fetch-native": "^1.6.4" } }, "sha512-5g7W5s6Gq+HDxwULGFQ861yAnEx3yd9V8GDwS96HBZ1nM1u93vN+KTuwXvNsV7Z3FJmCrD/pgU8WakvchclYuA=="],
|
||||||
|
|
||||||
|
"@hattip/walk": ["@hattip/walk@0.0.49", "", { "dependencies": { "@hattip/headers": "0.0.49", "cac": "^6.7.14", "mime-types": "^2.1.35" }, "bin": { "hattip-walk": "cli.js" } }, "sha512-AgJgKLooZyQnzMfoFg5Mo/aHM+HGBC9ExpXIjNqGimYTRgNbL/K7X5EM1kR2JY90BNKk9lo6Usq1T/nWFdT7TQ=="],
|
||||||
|
|
||||||
|
"@hiogawa/tiny-rpc": ["@hiogawa/tiny-rpc@0.2.3-pre.18", "", {}, "sha512-BiNHrutG9G9yV622QvkxZxF+PhkaH2Aspp4/X1KYTfnaQTcg4fFUTBWf5Kf533swon2SuVJwi6U6H1LQbhVOQQ=="],
|
||||||
|
|
||||||
|
"@hiogawa/utils": ["@hiogawa/utils@1.7.0", "", {}, "sha512-ghiEFWBR1NENoHn+lSuW7liicTIzVPN+8Srm5UedCTw43gus0mlse6Wp2lz6GmbOXJ/CalMPp/0Tz2X8tajkAg=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||||
|
|
||||||
|
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
||||||
|
|
||||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
|
||||||
|
|
||||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
|
||||||
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
|
||||||
|
|
||||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
|
||||||
|
|
||||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
|
||||||
|
|
||||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
|
||||||
|
|
||||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
|
||||||
|
|
||||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
|
||||||
|
|
||||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
|
||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
@@ -223,123 +197,167 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
"@pinia/colada": ["@pinia/colada@0.21.6", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-DppfAYky3Uavlpdx2iZHgd/+ZVPyBGTR+x+kFfAUz8h9l1DIQgf2cw/QZg0RZ4GAUNnKf6Ue6FzfWttwqhZXUQ=="],
|
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||||
|
|
||||||
|
"@kamilkisiela/fast-url-parser": ["@kamilkisiela/fast-url-parser@1.1.4", "", {}, "sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.115.0", "", { "os": "android", "cpu": "arm" }, "sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.115.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lWRX75u+gqfB4TF3pWCHuvhaeneAmRl2b2qNBcl4S6yJ0HtnT4VXOMEZrq747i4Zby1ZTxj6mtOe678Bg8gRLw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.115.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ii/oOZjfGY1aszXTy29Z5DRyCEnBOrAXDVCvfdfXFQsOZlbbOa7NMHD7D+06YFe5qdxfmbWAYv4yn6QJi/0d2g=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.115.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-R/sW/p8l77wglbjpMcF+h/3rWbp9zk1mRP3U14mxTYIC2k3m+aLBpXXgk2zksqf9qKk5mcc4GIYsuCn9l8TgDg=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.115.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-CSJ5ldNm9wIGGkhaIJeGmxRMZbgxThRN+X1ufYQQUNi5jZDV/U3C2QDMywpP93fczNBj961hXtcUPO/oVGq4Pw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.115.0", "", { "os": "linux", "cpu": "arm" }, "sha512-uWFwssE5dHfQ8lH+ktrsD9JA49+Qa0gtxZHUs62z1e91NgGz6O7jefHGI6aygNyKNS45pnnBSDSP/zV977MsOQ=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.115.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fZbqt8y/sKQ+v6bBCuv/mYYFoC0+fZI3mGDDEemmDOhT78+aUs2+4ZMdbd2btlXmnLaScl37r8IRbhnok5Ka9w=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.115.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1ej/MjuTY9tJEunU/hUPIFmgH5PqgMQoRjNOvOkibtJ3Zqlw/+Lc+HGHDNET8sjbgIkWzdhX+p4J96A5CPdbag=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.115.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HjsZbJPH9mMd4swJRywVMsDZsJX0hyKb1iNHo5ijRl5yhtbO3lj7ImSrrL1oZ1VEg0te4iKmDGGz/6YPLd1G8w=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.115.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zhhePoBrd7kQx3oClX/W6NldsuCbuMqaN9rRsY+6/WoorAb4j490PG/FjqgAXscWp2uSW2WV9L+ksn0wHrvsrg=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.115.0", "", { "os": "linux", "cpu": "none" }, "sha512-t/IRojvUE9XrKu+/H1b8YINug+7Q6FLls5rsm2lxB5mnS8GN/eYAYrPgHkcg9/1SueRDSzGpDYu3lGWTObk1zw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.115.0", "", { "os": "linux", "cpu": "none" }, "sha512-79jBHSSh/YpQRAmvYoaCfpyToRbJ/HBrdB7hxK2ku2JMehjopTVo+xMJss/RV7/ZYqeezgjvKDQzapJbgcjVZA=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.115.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-nA1TpxkhNTIOMMyiSSsa7XIVJVoOU/SsVrHIz3gHvWweB5PHCQfO7w+Lb2EP0lBWokv7HtA/KbF7aLDoXzmuMw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.115.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9iVX789DoC3SaOOG+X6NcF/tVChgLp2vcHffzOC2/Z1JTPlz6bMG2ogvcW6/9s0BG2qvhNQImd+gbWYeQbOwVw=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.115.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RmQmk+mjCB0nMNfEYhaCxwofLo1Z95ebHw1AGvRiWGCd4zhCNOyskgCbMogIcQzSB3SuEKWgkssyaiQYVAA4hQ=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.115.0", "", { "os": "none", "cpu": "arm64" }, "sha512-viigraWWQhhDvX5aGq+wrQq58k00Xq3MHz/0R4AFMxGlZ8ogNonpEfNc73Q5Ly87Z6sU9BvxEdG0dnYTfVnmew=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.115.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-IzGCrMwXhpb4kTXy/8lnqqqwjI7eOvy+r9AhVw+hsr8t1ecBBEHprcNy0aKatFHN6hsX7UMHHQmBAQjVvL/p1A=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.115.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/ym+Absk/TLFvbhh3se9XYuI1D7BrUVHw4RaG/2dmWKgBenrZHaJsgnRb7NJtaOyjEOLIPtULx1wDdVL0SX2eg=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.115.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-AQSZjIR+b+Te7uaO/hGTMjT8/oxlYrvKrOTi4KTHF/O6osjHEatUQ3y6ZW2+8+lJxy20zIcGz6iQFmFq/qDKkg=="],
|
||||||
|
|
||||||
|
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.115.0", "", { "os": "win32", "cpu": "x64" }, "sha512-oxUl82N+fIO9jIaXPph8SPPHQXrA08BHokBBJW8ct9F/x6o6bZE6eUAhUtWajbtvFhL8UYcCWRMba+kww6MBlA=="],
|
||||||
|
|
||||||
|
"@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||||
|
|
||||||
|
"@pinia/colada": ["@pinia/colada@1.0.0", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-YKSybA6wusFK4CAUPzItoSgPCfScVnnnO2MSlmaaisE/L7luE77GxFyhTzipM8IbvbXh4zkCy97OE7w9WX34wA=="],
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
||||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||||
|
|
||||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||||
|
|
||||||
|
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||||
|
|
||||||
|
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||||
|
|
||||||
|
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||||
|
|
||||||
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
|
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.6", "", { "os": "android", "cpu": "arm64" }, "sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm" }, "sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.6", "", { "os": "linux", "cpu": "x64" }, "sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.6", "", { "os": "none", "cpu": "arm64" }, "sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.6", "", { "os": "win32", "cpu": "x64" }, "sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
|
"@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
|
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
|
|
||||||
|
|
||||||
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
|
||||||
|
|
||||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="],
|
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="],
|
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||||
|
|
||||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||||
|
|
||||||
"@unhead/vue": ["@unhead/vue@2.1.9", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.9" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-7SqqDEn5zFID1PnEdjLCLa/kOhoAlzol0JdYfVr2Ejek+H4ON4s8iyExv2QQ8bReMosbXQ/Bw41j2CF1NUuGSA=="],
|
"@unhead/vue": ["@unhead/vue@2.1.12", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.12" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g=="],
|
||||||
|
|
||||||
"@unocss/cli": ["@unocss/cli@66.6.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.2", "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2", "@unocss/preset-wind4": "66.6.2", "@unocss/transformer-directives": "66.6.2", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-N7nKnOJ/36FRs3PE7+CFbzg7UBhIsucYYAK5xjJScX0H2q8O6rODaNM5uvc77Qh4q+y1S/Bt5ArOwIewzdpP4w=="],
|
"@unocss/cli": ["@unocss/cli@66.6.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.6", "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6", "@unocss/preset-wind4": "66.6.6", "@unocss/transformer-directives": "66.6.6", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-78SY8j4hAVelK+vP/adsDGaSjEITasYLFECJLHWxUJSzK+G9UIc5wtL/u4jA+zKvwVkHcDvbkcO5K6wwwpAixg=="],
|
||||||
|
|
||||||
"@unocss/config": ["@unocss/config@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-qny2bRW1OA+MZbWShVZdBg6fJundm1LqQwCxJnIpeK3McpPHS3pnHBiwD1wfZHY2z5Pe+XgZOZkozNmG/eyyqg=="],
|
"@unocss/config": ["@unocss/config@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-menlnkqAFX/4wR2aandY8hSqrt01JE+rOzvtQxWaBt8kf1du62b0sS72FE5Z40n6HlEsEbF91N9FCfhnzG6i6g=="],
|
||||||
|
|
||||||
"@unocss/core": ["@unocss/core@66.6.2", "", {}, "sha512-IOvN1BLRP0VTjjS5afSxmXhvKRDko2Shisp8spU+A9qiH1tXEFP3phyVevm/SuGwBHO1lC+SJ451/4oFkCAwJA=="],
|
"@unocss/core": ["@unocss/core@66.6.6", "", {}, "sha512-Sbbx0ZQqmV8K2lg8E+z9MJzWb1MgRtJnvqzxDIrNuBjXasKhbcFt5wEMBtEZJOr63Z4ck0xThhZK53HmYT2jmg=="],
|
||||||
|
|
||||||
"@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-D2tK/8QClrVViSuoH5eLjXwlVOK1UgXx7ukz/D260+R6vhCmjv97RXPouZkq40sxGzfxzaQZUyPEjXLjtnO3bw=="],
|
"@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-uMzekF2miZRUwSZGvy3yYQiBAcSAs9LiXK8e3NjldxEw8xcRDWgTErxgStRoBeAD6UyzDcg/Cvwtf2guMbtR+g=="],
|
||||||
|
|
||||||
"@unocss/inspector": ["@unocss/inspector@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-q0kktb01dXeeXyNnNwYM1SkSHxrEOQhCZ/YQ5aCdC7BWNGF4yZMK0YrJXmGUTEHN4RhEPLN/rAIsDBsKcoFaAQ=="],
|
"@unocss/inspector": ["@unocss/inspector@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-CpXIsqHwxCXJtUjUz6S29diHCIA+EJ1u5WML/6m2YPI4ObgWAVKrExy09inSg2icS52lFkWWdWQSeqc9kl5W6Q=="],
|
||||||
|
|
||||||
"@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-pRry38qO1kJvj5/cekbDk0QLosty+UFQ3fhNiph88D//jkT5tsUCn77nB/RTSe7oTqw/FqNwxPgbGz/wfNWqZg=="],
|
"@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-3H12UI1rBt60PQy+S4IEeFYWu1/WQFuc2yhJ5mu/RCvX5/qwlIGanBpuh+xzTPXU1fWBlZN68yyO9uWOQgTqZQ=="],
|
||||||
|
|
||||||
"@unocss/preset-icons": ["@unocss/preset-icons@66.6.2", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.2", "ofetch": "^1.5.1" } }, "sha512-FjhxvYX+21HefYdMIxJCq8C9v/K7fSlO1DMqDQgtrCp0/WvHyFncHILLOwp064M7m3AqzOVJx7Vw/zCvKy0Jrg=="],
|
"@unocss/preset-icons": ["@unocss/preset-icons@66.6.6", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.6", "ofetch": "^1.5.1" } }, "sha512-HfIEEqf3jyKexOB2Sux556n0NkPoUftb2H4+Cf7prJvKHopMkZ/OUkXjwvUlxt1e5UpAEaIa0A2Ir7+ApxXoGA=="],
|
||||||
|
|
||||||
"@unocss/preset-mini": ["@unocss/preset-mini@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/extractor-arbitrary-variants": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-mybpiAq9htF7PWPH1Mnb4y7hrxVwpsBg8VfbjSglY3SfLca8RrJtvBT+DVh7YUDRiYsZGfihRWkfD0AN68gkcA=="],
|
"@unocss/preset-mini": ["@unocss/preset-mini@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/extractor-arbitrary-variants": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-k+/95PKMPOK57cJcSmz34VkIFem8BlujRRx6/L0Yusw7vLJMh98k0rPhC5s+NomZ/d9ZPgbNylskLhItJlak3w=="],
|
||||||
|
|
||||||
"@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-ybb45So2x87P3bssLRp1uIS+VHAeNSecwkHqiv93PnuBDJ38/9XlqWF98uga2MEfNM3zvMj9plX9MauidxiPrw=="],
|
"@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-KgBXYPYS0g4TVC3NLiIB78YIqUlvDLanz1EHIDo34rOTUfMgY8Uf5VuDJAzMu4Sc0LiwwBJbk6nIG9/Zm7ufWg=="],
|
||||||
|
|
||||||
"@unocss/preset-typography": ["@unocss/preset-typography@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-1f/ZfeuLQOnO48mRz1+6UdoJxa13ZYcamaLz7ft96n7D1eWvkOUAC/AUUke/kbHh3vvqwRVimC9OpdXxdGFQAQ=="],
|
"@unocss/preset-typography": ["@unocss/preset-typography@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-SM1km5nqt15z4sTabfOobSC633I5Ol5nnme6JFTra4wiyCUNs+Cg31nJ6jnopWDUT4SEAXqfUH7jKSSoCnI6ZA=="],
|
||||||
|
|
||||||
"@unocss/preset-uno": ["@unocss/preset-uno@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2" } }, "sha512-Wy3V25ZF29OmVHJk5ghP6HCCRNBJXm0t+bKLKJJknOjD+/D51DZbUsDqZBtTpVtgi/SOPDbw7cX3lY2oqt4Hnw=="],
|
"@unocss/preset-uno": ["@unocss/preset-uno@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6" } }, "sha512-40PcBDtlhW7QP7e/WOxC684IhN5T1dXvj1dgx9ZzK+8lEDGjcX7bN2noW4aSenzSrHymeSsMrL/0ltL4ED/5Zw=="],
|
||||||
|
|
||||||
"@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "ofetch": "^1.5.1" } }, "sha512-0ckqiE8HkhETeghhxCXVGf96sNPhgBsB5q32iAuMM0HFR4x+ANiLqyfKrm/iqxKUw6rVO4+ItTV0RUWKcZvkXg=="],
|
"@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "ofetch": "^1.5.1" } }, "sha512-5ikwgrJB8VPzKd0bqgGNgYUGix90KFnVtKJPjWTP5qsv3+ZtZnea1rRbAFl8i2t52hg35msNBsQo+40IC3xB6A=="],
|
||||||
|
|
||||||
"@unocss/preset-wind": ["@unocss/preset-wind@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2" } }, "sha512-G0H4baUizmTByEowqGuYbKpU2TTisDhZ9W7hrIpYFbRkFv0i1kN2mIxCwj/FLmdY/6x8iSRJ7rO8Nez63YYhnw=="],
|
"@unocss/preset-wind": ["@unocss/preset-wind@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6" } }, "sha512-TMy3lZ35FP/4QqDHOLWZmV+RoOGWUDqnDEOTjOKI1CQARGta0ppUmq+IZMuI1ZJLuOa4OZ9V6SfnwMXwRLgXmw=="],
|
||||||
|
|
||||||
"@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-mini": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-UqdU2Obx3wXid9xeBHGY1MWxedXa43MGuP5Z2FA9modcXptReux4Zhy764SeQwx6acOUEql2/CTvOBwelZzheQ=="],
|
"@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-mini": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-rk6gPPIQ7z2DVucOqp7XZ4vGpKAuzBV1vtUDvDh5WscxzO/QlqaeTfTALk5YgGpmLaF4+ns6FrTgLjV+wHgHuQ=="],
|
||||||
|
|
||||||
"@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/extractor-arbitrary-variants": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-XU+4NN9QIMefawDB9FqOeKONXeGDUJQuQgOeBcpbV/jwOYtyqRrHiqQg++fy1hRbluM+S+KqwRHYjvje8zCTow=="],
|
"@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/extractor-arbitrary-variants": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-caTDM9rZSlp4tyPWWAnwMvQr2PXq53LsEYwd3N8zj0ou2hcsqptJvF+mFvyhvGF66x26wWJr/FwuUEhh7qycaw=="],
|
||||||
|
|
||||||
"@unocss/rule-utils": ["@unocss/rule-utils@66.6.2", "", { "dependencies": { "@unocss/core": "^66.6.2", "magic-string": "^0.30.21" } }, "sha512-cygfCtkeMrqMM6si1cnyOF16sS7M2gCAqgmZybAhGV7tmH7V8Izn52JZiZIrxVRNMz9dWMVWerHEI9nLbFdbrg=="],
|
"@unocss/rule-utils": ["@unocss/rule-utils@66.6.6", "", { "dependencies": { "@unocss/core": "^66.6.6", "magic-string": "^0.30.21" } }, "sha512-krWtQKGshOaqQMuxeGq1NOA8NL35VdpYlmQEWOe39BY6TACT51bgQFu40MRfsAIMZZtoGS2YYTrnHojgR92omw=="],
|
||||||
|
|
||||||
"@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.2", "", { "dependencies": { "@babel/parser": "7.27.7", "@babel/traverse": "7.27.7", "@unocss/core": "66.6.2" } }, "sha512-WiAEdEowGjQWu1ayhkGGBNGyw3mZLzZ+V5o3zx5U2GPuqvP67YIUfvY+/gTkCnd4+A8unkb+a1VeVgr4cHUkQw=="],
|
"@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "oxc-parser": "^0.115.0", "oxc-walker": "^0.7.0" } }, "sha512-NnDchmN2EeFLy4lfVqDgNe9j1+w2RLL2L9zKECXs5g6rDVfeeEK6FNgxSq3XnPcKltjNCy1pF4MaDOROG7r8yA=="],
|
||||||
|
|
||||||
"@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-L0yaQAmvWkm6LVLXMviqhHIi4c7WQpZFBgJF8jfsALyHihh8K9U9OrRJ81zfLH3Ltw5ZbGzoDE8m/2bB6aRhyw=="],
|
"@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-KKssJxU8fZ9x84yznIirbtta2sB0LN/3lm0bp+Wl1298HITaNiVeG2n26iStQ3N7r240xRN2RarxncSVCMFwWw=="],
|
||||||
|
|
||||||
"@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2", "css-tree": "^3.1.0" } }, "sha512-gjLDLItTUJ4CV8K2AA0cw381a7rJ3U4kCHQmZmN3+956o2R7cEHSLyEczmMy04Mg2JBomrjIZjo+L66z5rvblQ=="],
|
"@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6", "css-tree": "^3.1.0" } }, "sha512-CReFTcBfMtKkRvzIqxL20VptWt5C1Om27dwoKzyVFBXv0jzViWysbu0y0AQg3bsgD4cFqndFyAGyeL84j0nbKg=="],
|
||||||
|
|
||||||
"@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-Uoo6xthOHJ36NdN4b7s/Y7R3fZOf4JYgKzuldHEyHAo0LL204Ss+Ah0+TEt4v72aq+Z86vrLJPyYCeGNKdr8cA=="],
|
"@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-j4L/0Tw6AdMVB2dDnuBlDbevyL1/0CAk88a77VF/VjgEIBwB9VXsCCUsxz+2Dohcl7N2GMm7+kpaWA6qt2PSaA=="],
|
||||||
|
|
||||||
"@unocss/vite": ["@unocss/vite@66.6.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.2", "@unocss/core": "66.6.2", "@unocss/inspector": "66.6.2", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-HLmzDvde3BJ2C6iromHVE21lmNm4SmGSMlbSbFuLPOmWV11XhhHBkAOzytSxPBRG0dbuo+InSGUM14Ek2d6UDg=="],
|
"@unocss/vite": ["@unocss/vite@66.6.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.6", "@unocss/core": "66.6.6", "@unocss/inspector": "66.6.6", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-DgG7KcUUMtoDhPOlFf2l4dR+66xZ23SdZvTYpikk5nZfLCzZd62vedutD7x0bTR6VpK2YRq39B+F+Z6TktNY/w=="],
|
||||||
|
|
||||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
|
||||||
|
|
||||||
@@ -353,13 +371,13 @@
|
|||||||
|
|
||||||
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@2.0.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.4", "@vue/compiler-sfc": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ=="],
|
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@2.0.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.4", "@vue/compiler-sfc": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ=="],
|
||||||
|
|
||||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
"@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
||||||
|
|
||||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
|
||||||
|
|
||||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
|
||||||
|
|
||||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||||
|
|
||||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
||||||
|
|
||||||
@@ -367,15 +385,15 @@
|
|||||||
|
|
||||||
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
||||||
|
|
||||||
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
|
"@vue/reactivity": ["@vue/reactivity@3.5.30", "", { "dependencies": { "@vue/shared": "3.5.30" } }, "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q=="],
|
||||||
|
|
||||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
|
"@vue/runtime-core": ["@vue/runtime-core@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg=="],
|
||||||
|
|
||||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/runtime-core": "3.5.29", "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg=="],
|
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw=="],
|
||||||
|
|
||||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.29", "", { "dependencies": { "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "vue": "3.5.29" } }, "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g=="],
|
"@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
|
||||||
|
|
||||||
"@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
"@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
||||||
|
|
||||||
"@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
|
"@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
|
||||||
|
|
||||||
@@ -383,8 +401,16 @@
|
|||||||
|
|
||||||
"@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="],
|
"@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="],
|
||||||
|
|
||||||
|
"@whatwg-node/fetch": ["@whatwg-node/fetch@0.9.23", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.6.0", "urlpattern-polyfill": "^10.0.0" } }, "sha512-7xlqWel9JsmxahJnYVUj/LLxWcnA93DR4c9xlw3U814jWTiYalryiH1qToik1hOxweKKRLi4haXHM5ycRksPBA=="],
|
||||||
|
|
||||||
|
"@whatwg-node/node-fetch": ["@whatwg-node/node-fetch@0.6.0", "", { "dependencies": { "@kamilkisiela/fast-url-parser": "^1.1.4", "busboy": "^1.6.0", "fast-querystring": "^1.1.1", "tslib": "^2.6.3" } }, "sha512-tcZAhrpx6oVlkEsRngeTEEE7I5/QdLjeEz4IlekabGaESP7+Dkm/6a9KcF1KdCBB7mO9PXtBkwCuTCt8+UPg8Q=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
|
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
|
||||||
|
|
||||||
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
|
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
|
||||||
@@ -395,18 +421,26 @@
|
|||||||
|
|
||||||
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||||
|
|
||||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||||
|
|
||||||
|
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||||
@@ -415,10 +449,10 @@
|
|||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
|
||||||
|
|
||||||
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||||
|
|
||||||
|
"cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="],
|
||||||
|
|
||||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
@@ -435,9 +469,9 @@
|
|||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||||
|
|
||||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||||
|
|
||||||
@@ -445,24 +479,36 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||||
|
|
||||||
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
|
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||||
|
|
||||||
"hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="],
|
"hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="],
|
||||||
|
|
||||||
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
|
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
|
||||||
|
|
||||||
|
"i18next": ["i18next@25.8.18", "", { "dependencies": { "@babel/runtime": "^7.28.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA=="],
|
||||||
|
|
||||||
|
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
|
||||||
|
|
||||||
|
"i18next-vue": ["i18next-vue@5.4.0", "", { "peerDependencies": { "i18next": ">=23", "vue": "^3.4.38" } }, "sha512-GDj0Xvmis5Xgcvo9gMBJMgJCtewYMLZP6gAEPDDGCMjA+QeB4uS4qUf1MK79mkz/FukhaJdC+nlj0y1qk6NO2Q=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
"is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="],
|
"is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="],
|
||||||
|
|
||||||
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
||||||
@@ -475,19 +521,49 @@
|
|||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||||
|
|
||||||
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||||
|
|
||||||
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"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=="],
|
"magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="],
|
||||||
|
|
||||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||||
|
|
||||||
"miniflare": ["miniflare@4.20260302.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260302.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-joGFywlo7HdfHXXGOkc6tDCVkwjEncM0mwEsMOLWcl+vDVJPj9HRV7JtEa0+lCpNOLdYw7mZNHYe12xz9KtJOw=="],
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
@@ -501,6 +577,8 @@
|
|||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
@@ -509,9 +587,11 @@
|
|||||||
|
|
||||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
"oxc-parser": ["oxc-parser@0.115.0", "", { "dependencies": { "@oxc-project/types": "^0.115.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.115.0", "@oxc-parser/binding-android-arm64": "0.115.0", "@oxc-parser/binding-darwin-arm64": "0.115.0", "@oxc-parser/binding-darwin-x64": "0.115.0", "@oxc-parser/binding-freebsd-x64": "0.115.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.115.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.115.0", "@oxc-parser/binding-linux-arm64-gnu": "0.115.0", "@oxc-parser/binding-linux-arm64-musl": "0.115.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.115.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.115.0", "@oxc-parser/binding-linux-riscv64-musl": "0.115.0", "@oxc-parser/binding-linux-s390x-gnu": "0.115.0", "@oxc-parser/binding-linux-x64-gnu": "0.115.0", "@oxc-parser/binding-linux-x64-musl": "0.115.0", "@oxc-parser/binding-openharmony-arm64": "0.115.0", "@oxc-parser/binding-wasm32-wasi": "0.115.0", "@oxc-parser/binding-win32-arm64-msvc": "0.115.0", "@oxc-parser/binding-win32-ia32-msvc": "0.115.0", "@oxc-parser/binding-win32-x64-msvc": "0.115.0" } }, "sha512-2w7Xn3CbS/zwzSY82S5WLemrRu3CT57uF7Lx8llrE/2bul6iMTcJE4Rbls7GDNbLn3ttATI68PfOz2Pt3KZ2cQ=="],
|
||||||
|
|
||||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
"oxc-walker": ["oxc-walker@0.7.0", "", { "dependencies": { "magic-regexp": "^0.10.0" }, "peerDependencies": { "oxc-parser": ">=0.98.0" } }, "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A=="],
|
||||||
|
|
||||||
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
@@ -527,32 +607,40 @@
|
|||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||||
|
|
||||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
|
|
||||||
|
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
"rolldown": ["rolldown@1.0.0-rc.6", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.6" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-x64": "1.0.0-rc.6", "@rolldown/binding-freebsd-x64": "1.0.0-rc.6", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.6", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.6", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.6", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.6", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.6", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.6" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw=="],
|
||||||
|
|
||||||
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
|
||||||
|
|
||||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||||
|
|
||||||
|
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
||||||
|
|
||||||
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
@@ -561,25 +649,27 @@
|
|||||||
|
|
||||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="],
|
||||||
|
|
||||||
|
"type-level-regexp": ["type-level-regexp@0.1.17", "", {}, "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||||
|
|
||||||
"unconfig": ["unconfig@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.5.0" } }, "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA=="],
|
"unconfig": ["unconfig@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.5.0" } }, "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA=="],
|
||||||
|
|
||||||
"unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="],
|
"unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="],
|
||||||
|
|
||||||
"undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
|
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
"unhead": ["unhead@2.1.12", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA=="],
|
||||||
|
|
||||||
"unhead": ["unhead@2.1.9", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-4GvP6YeJQzo9J3g9fFZUJOH6jacUp5JgJ0/zC8eZrt8Dwompg9SuOSfrYbZaEzsfMPgQc4fsEjMoY9WzGPOChg=="],
|
|
||||||
|
|
||||||
"unimport": ["unimport@5.6.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A=="],
|
"unimport": ["unimport@5.6.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A=="],
|
||||||
|
|
||||||
"unocss": ["unocss@66.6.2", "", { "dependencies": { "@unocss/cli": "66.6.2", "@unocss/core": "66.6.2", "@unocss/preset-attributify": "66.6.2", "@unocss/preset-icons": "66.6.2", "@unocss/preset-mini": "66.6.2", "@unocss/preset-tagify": "66.6.2", "@unocss/preset-typography": "66.6.2", "@unocss/preset-uno": "66.6.2", "@unocss/preset-web-fonts": "66.6.2", "@unocss/preset-wind": "66.6.2", "@unocss/preset-wind3": "66.6.2", "@unocss/preset-wind4": "66.6.2", "@unocss/transformer-attributify-jsx": "66.6.2", "@unocss/transformer-compile-class": "66.6.2", "@unocss/transformer-directives": "66.6.2", "@unocss/transformer-variant-group": "66.6.2", "@unocss/vite": "66.6.2" }, "peerDependencies": { "@unocss/astro": "66.6.2", "@unocss/postcss": "66.6.2", "@unocss/webpack": "66.6.2" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-ulkfFBFm++/yTdgDn/clpxtm3GxynZi57F4KETQkMQWRXUI7FwqPKGn0xooscvbtldlX67pkovwj/mzkwExitQ=="],
|
"unocss": ["unocss@66.6.6", "", { "dependencies": { "@unocss/cli": "66.6.6", "@unocss/core": "66.6.6", "@unocss/preset-attributify": "66.6.6", "@unocss/preset-icons": "66.6.6", "@unocss/preset-mini": "66.6.6", "@unocss/preset-tagify": "66.6.6", "@unocss/preset-typography": "66.6.6", "@unocss/preset-uno": "66.6.6", "@unocss/preset-web-fonts": "66.6.6", "@unocss/preset-wind": "66.6.6", "@unocss/preset-wind3": "66.6.6", "@unocss/preset-wind4": "66.6.6", "@unocss/transformer-attributify-jsx": "66.6.6", "@unocss/transformer-compile-class": "66.6.6", "@unocss/transformer-directives": "66.6.6", "@unocss/transformer-variant-group": "66.6.6", "@unocss/vite": "66.6.6" }, "peerDependencies": { "@unocss/astro": "66.6.6", "@unocss/postcss": "66.6.6", "@unocss/webpack": "66.6.6" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-PRKK945e2oZKHV664MA5Z9CDHbvY/V79IvTOUWKZ514jpl3UsJU3sS+skgxmKJSmwrWvXE5OVcmPthJrD/7vxg=="],
|
||||||
|
|
||||||
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
||||||
@@ -591,53 +681,57 @@
|
|||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@8.0.0-beta.16", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.6", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.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-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q=="],
|
||||||
|
|
||||||
"vite-ssr-components": ["vite-ssr-components@0.5.2", "", { "dependencies": { "@babel/parser": "^7.27.2", "@babel/traverse": "^7.27.1", "picomatch": "^4.0.2" } }, "sha512-1a8YThRwyyu1gGjc1Ral9Q4uS+n0D4GydhbkVd9c1SA1YNgXyrOizttped87C1ItEznQzhiCyQjaOcYnXa0zMA=="],
|
"vite-ssr-components": ["vite-ssr-components@0.5.2", "", { "dependencies": { "@babel/parser": "^7.27.2", "@babel/traverse": "^7.27.1", "picomatch": "^4.0.2" } }, "sha512-1a8YThRwyyu1gGjc1Ral9Q4uS+n0D4GydhbkVd9c1SA1YNgXyrOizttped87C1ItEznQzhiCyQjaOcYnXa0zMA=="],
|
||||||
|
|
||||||
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
|
"vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="],
|
||||||
|
|
||||||
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="],
|
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
|
|
||||||
"workerd": ["workerd@1.20260302.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260302.0", "@cloudflare/workerd-darwin-arm64": "1.20260302.0", "@cloudflare/workerd-linux-64": "1.20260302.0", "@cloudflare/workerd-linux-arm64": "1.20260302.0", "@cloudflare/workerd-windows-64": "1.20260302.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-FhNdC8cenMDllI6bTktFgxP5Bn5ZEnGtofgKipY6pW9jtq708D1DeGI6vGad78KQLBGaDwFy1eThjCoLYgFfog=="],
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
"wrangler": ["wrangler@4.68.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.14.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260302.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260302.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260302.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-G+TI3k/olEGBAVkPtUlhAX/DIbL/190fv3aK+r+45/wPclNEymjxCc35T8QGTDhc2fEMXiw51L5bH9aNsBg+yQ=="],
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||||
|
|
||||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
|
||||||
|
|
||||||
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||||
|
|
||||||
"@unocss/transformer-attributify-jsx/@babel/parser": ["@babel/parser@7.27.7", "", { "dependencies": { "@babel/types": "^7.27.7" }, "bin": "./bin/babel-parser.js" }, "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q=="],
|
|
||||||
|
|
||||||
"@unocss/transformer-attributify-jsx/@babel/traverse": ["@babel/traverse@7.27.7", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw=="],
|
|
||||||
|
|
||||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
|
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
|
||||||
|
|
||||||
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||||
|
|
||||||
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||||
|
|
||||||
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||||
|
|
||||||
|
"@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
|
|
||||||
"@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
"@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=="],
|
"@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
|
"magic-regexp/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.6", "", {}, "sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA=="],
|
||||||
|
|
||||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
|
|
||||||
@@ -645,11 +739,27 @@
|
|||||||
|
|
||||||
"unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
"unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||||
|
|
||||||
|
"unimport/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
||||||
|
|
||||||
"vue-router/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
|
"vue-router/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
|
||||||
|
|
||||||
"@unocss/transformer-attributify-jsx/@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||||
|
|
||||||
|
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||||
|
|
||||||
|
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||||
|
|
||||||
|
"@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||||
|
|
||||||
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||||
|
|
||||||
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||||
|
|
||||||
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||||
|
|
||||||
|
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||||
|
|
||||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
|
|||||||
40
components.d.ts
vendored
40
components.d.ts
vendored
@@ -17,15 +17,18 @@ declare module 'vue' {
|
|||||||
AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
||||||
AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
||||||
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||||
AppButton: typeof import('./src/components/app/AppButton.vue')['default']
|
AppButton: typeof import('./src/components/ui/AppButton.vue')['default']
|
||||||
AppConfirmHost: typeof import('./src/components/app/AppConfirmHost.vue')['default']
|
AppConfirmHost: typeof import('./src/components/ui/AppConfirmHost.vue')['default']
|
||||||
AppDialog: typeof import('./src/components/app/AppDialog.vue')['default']
|
AppDialog: typeof import('./src/components/ui/AppDialog.vue')['default']
|
||||||
AppInput: typeof import('./src/components/app/AppInput.vue')['default']
|
AppInput: typeof import('./src/components/ui/AppInput.vue')['default']
|
||||||
AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default']
|
AppProgressBar: typeof import('./src/components/ui/AppProgressBar.vue')['default']
|
||||||
AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default']
|
AppSwitch: typeof import('./src/components/ui/AppSwitch.vue')['default']
|
||||||
AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default']
|
AppToastHost: typeof import('./src/components/ui/AppToastHost.vue')['default']
|
||||||
|
AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default']
|
||||||
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
|
AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||||
|
BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||||
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||||
@@ -34,6 +37,7 @@ declare module 'vue' {
|
|||||||
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||||
ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
||||||
CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
||||||
|
copy: typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||||
Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
||||||
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||||
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||||
@@ -57,6 +61,7 @@ declare module 'vue' {
|
|||||||
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||||
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||||
|
OfflineOverlay: typeof import('./src/components/OfflineOverlay.vue')['default']
|
||||||
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||||
PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
||||||
@@ -77,6 +82,7 @@ declare module 'vue' {
|
|||||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||||
UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
||||||
|
'UserIcon copy': typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||||
Video: typeof import('./src/components/icons/Video.vue')['default']
|
Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||||
VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
||||||
VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
||||||
@@ -96,15 +102,18 @@ declare global {
|
|||||||
const AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
const AdvertisementIcon: typeof import('./src/components/icons/AdvertisementIcon.vue')['default']
|
||||||
const AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
const AlertTriangle: typeof import('./src/components/icons/AlertTriangle.vue')['default']
|
||||||
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||||
const AppButton: typeof import('./src/components/app/AppButton.vue')['default']
|
const AppButton: typeof import('./src/components/ui/AppButton.vue')['default']
|
||||||
const AppConfirmHost: typeof import('./src/components/app/AppConfirmHost.vue')['default']
|
const AppConfirmHost: typeof import('./src/components/ui/AppConfirmHost.vue')['default']
|
||||||
const AppDialog: typeof import('./src/components/app/AppDialog.vue')['default']
|
const AppDialog: typeof import('./src/components/ui/AppDialog.vue')['default']
|
||||||
const AppInput: typeof import('./src/components/app/AppInput.vue')['default']
|
const AppInput: typeof import('./src/components/ui/AppInput.vue')['default']
|
||||||
const AppProgressBar: typeof import('./src/components/app/AppProgressBar.vue')['default']
|
const AppProgressBar: typeof import('./src/components/ui/AppProgressBar.vue')['default']
|
||||||
const AppSwitch: typeof import('./src/components/app/AppSwitch.vue')['default']
|
const AppSwitch: typeof import('./src/components/ui/AppSwitch.vue')['default']
|
||||||
const AppToastHost: typeof import('./src/components/app/AppToastHost.vue')['default']
|
const AppToastHost: typeof import('./src/components/ui/AppToastHost.vue')['default']
|
||||||
|
const AppTopLoadingBar: typeof import('./src/components/AppTopLoadingBar.vue')['default']
|
||||||
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
|
const AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||||
|
const BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
const BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
const BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||||
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||||
@@ -113,6 +122,7 @@ declare global {
|
|||||||
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||||
const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
|
||||||
const CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
const CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default']
|
||||||
|
const copy: typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||||
const Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
const Credit: typeof import('./src/components/icons/Credit.vue')['default']
|
||||||
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||||
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||||
@@ -136,6 +146,7 @@ declare global {
|
|||||||
const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||||
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||||
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||||
|
const OfflineOverlay: typeof import('./src/components/OfflineOverlay.vue')['default']
|
||||||
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||||
const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
||||||
@@ -156,6 +167,7 @@ declare global {
|
|||||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
const UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
const UploadIcon: typeof import('./src/components/icons/UploadIcon.vue')['default']
|
||||||
const UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
const UserIcon: typeof import('./src/components/icons/UserIcon.vue')['default']
|
||||||
|
const 'UserIcon copy': typeof import('./src/components/icons/UserIcon copy.vue')['default']
|
||||||
const Video: typeof import('./src/components/icons/Video.vue')['default']
|
const Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||||
const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
const VideoIcon: typeof import('./src/components/icons/VideoIcon.vue')['default']
|
||||||
const VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
const VideoPlayIcon: typeof import('./src/components/icons/VideoPlayIcon.vue')['default']
|
||||||
|
|||||||
47
package.json
47
package.json
@@ -2,37 +2,46 @@
|
|||||||
"name": "holistream",
|
"name": "holistream",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun vite",
|
"dev": "bunx --bun vite",
|
||||||
"build": "bun vite build",
|
"build": "bunx --bun vite build",
|
||||||
"preview": "bun vite preview",
|
"preview": "bunx --bun vite preview"
|
||||||
"deploy": "wrangler deploy",
|
|
||||||
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
|
||||||
"tail": "wrangler tail"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pinia/colada": "^0.21.2",
|
"@bufbuild/protobuf": "^2.11.0",
|
||||||
"@unhead/vue": "^2.1.2",
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
"@vueuse/core": "^14.2.0",
|
"@hattip/adapter-node": "^0.0.49",
|
||||||
|
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||||
|
"@hiogawa/utils": "^1.7.0",
|
||||||
|
"@hono/node-server": "^1.19.11",
|
||||||
|
"@hono/zod-validator": "^0.7.6",
|
||||||
|
"@pinia/colada": "^1.0.0",
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
|
"@unhead/vue": "^2.1.12",
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"aws4fetch": "^1.0.20",
|
"aws4fetch": "^1.0.20",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"hono": "^4.11.7",
|
"hono": "^4.12.7",
|
||||||
|
"i18next": "^25.8.18",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"i18next-vue": "^5.4.0",
|
||||||
"is-mobile": "^5.0.0",
|
"is-mobile": "^5.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"tailwind-merge": "^3.4.0",
|
"superjson": "^2.2.6",
|
||||||
"vue": "^3.5.27",
|
"tailwind-merge": "^3.5.0",
|
||||||
"vue-router": "^5.0.2",
|
"tweetnacl": "^1.0.3",
|
||||||
|
"vue": "^3.5.30",
|
||||||
|
"vue-router": "^5.0.3",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.23.0",
|
"@types/bun": "^1.3.10",
|
||||||
"@types/node": "^25.2.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||||
"unocss": "^66.6.0",
|
"estree-walker": "2.0.2",
|
||||||
|
"unocss": "^66.6.6",
|
||||||
"unplugin-auto-import": "^21.0.0",
|
"unplugin-auto-import": "^21.0.0",
|
||||||
"unplugin-vue-components": "^31.0.0",
|
"unplugin-vue-components": "^31.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0-beta.16",
|
||||||
"vite-ssr-components": "^0.5.2",
|
"vite-ssr-components": "^0.5.2"
|
||||||
"wrangler": "^4.62.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
proto/v1/common.proto
Normal file
46
proto/v1/common.proto
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package stream.common.v1;
|
||||||
|
|
||||||
|
option go_package = "stream/proto/gen/go/common/v1;commonv1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
message RequestContext {
|
||||||
|
string user_id = 1;
|
||||||
|
string email = 2;
|
||||||
|
string role = 3;
|
||||||
|
string request_id = 4;
|
||||||
|
string source = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaginationRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 page_size = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaginationResponse {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 page_size = 2;
|
||||||
|
int64 total = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Money {
|
||||||
|
double amount = 1;
|
||||||
|
string currency = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Empty {}
|
||||||
|
|
||||||
|
message IdRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteResponse {
|
||||||
|
string message = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TimestampRange {
|
||||||
|
google.protobuf.Timestamp from = 1;
|
||||||
|
google.protobuf.Timestamp to = 2;
|
||||||
|
}
|
||||||
134
proto/v1/user.proto
Normal file
134
proto/v1/user.proto
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package stream.User.v1;
|
||||||
|
|
||||||
|
option go_package = "stream/proto/gen/go/User/v1;Userv1";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
service UserService {
|
||||||
|
// User CRUD
|
||||||
|
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||||
|
rpc GetUserByEmail(GetUserByEmailRequest) returns (GetUserResponse);
|
||||||
|
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
|
||||||
|
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
|
||||||
|
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
|
||||||
|
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
|
||||||
|
rpc UpdateUserPassword(UpdateUserPasswordRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
rpc GetPreferences(GetPreferencesRequest) returns (GetPreferencesResponse);
|
||||||
|
rpc UpsertPreferences(UpsertPreferencesRequest) returns (UpsertPreferencesResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── User Messages ───────────────────────────────────────────────────────────
|
||||||
|
message UpdateUserPasswordRequest {
|
||||||
|
string id = 1;
|
||||||
|
string new_password = 2;
|
||||||
|
}
|
||||||
|
message GetUserRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserByEmailRequest {
|
||||||
|
string email = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserResponse {
|
||||||
|
User user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUsersRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 page_size = 2;
|
||||||
|
string role = 3; // optional filter
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUsersResponse {
|
||||||
|
repeated User users = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 page_size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateUserRequest {
|
||||||
|
string email = 1;
|
||||||
|
optional string username = 2;
|
||||||
|
optional string password = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateUserResponse {
|
||||||
|
User user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserRequest {
|
||||||
|
string id = 1;
|
||||||
|
optional string username = 2;
|
||||||
|
optional string avatar = 3;
|
||||||
|
optional string role = 4;
|
||||||
|
optional string plan_id = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserResponse {
|
||||||
|
User user = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteUserRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteUserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Preferences Messages ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
message GetPreferencesRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPreferencesResponse {
|
||||||
|
Preferences preferences = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpsertPreferencesRequest {
|
||||||
|
Preferences preferences = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpsertPreferencesResponse {
|
||||||
|
Preferences preferences = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Core Models ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
message User {
|
||||||
|
string id = 1;
|
||||||
|
string email = 2;
|
||||||
|
string password = 3;
|
||||||
|
optional string username = 4;
|
||||||
|
optional string avatar = 5;
|
||||||
|
optional string role = 6;
|
||||||
|
optional string google_id = 7;
|
||||||
|
int64 storage_used = 8;
|
||||||
|
optional string plan_id = 9;
|
||||||
|
optional google.protobuf.Timestamp created_at = 10;
|
||||||
|
google.protobuf.Timestamp updated_at = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Preferences {
|
||||||
|
string user_id = 1;
|
||||||
|
optional string language = 2;
|
||||||
|
optional string locale = 3;
|
||||||
|
optional bool email_notifications = 4;
|
||||||
|
optional bool push_notifications = 5;
|
||||||
|
optional bool marketing_notifications = 6;
|
||||||
|
optional bool telegram_notifications = 7;
|
||||||
|
optional bool autoplay = 8;
|
||||||
|
optional bool loop = 9;
|
||||||
|
optional bool muted = 10;
|
||||||
|
optional bool show_controls = 11;
|
||||||
|
optional bool pip = 12;
|
||||||
|
optional bool airplay = 13;
|
||||||
|
optional bool chromecast = 14;
|
||||||
|
optional bool encrytion_m3u8 = 15;
|
||||||
|
}
|
||||||
1283
public/locales/en/translation.json
Normal file
1283
public/locales/en/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
1278
public/locales/vi/translation.json
Normal file
1278
public/locales/vi/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
7
scripts/gen-nacl-keys.ts
Normal file
7
scripts/gen-nacl-keys.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// scripts/gen-nacl-keys.ts
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
|
||||||
|
const kp = nacl.box.keyPair();
|
||||||
|
|
||||||
|
console.log("PUBLIC_KEY_BASE64=", Buffer.from(kp.publicKey).toString("base64"));
|
||||||
|
console.log("SECRET_KEY_BASE64=", Buffer.from(kp.secretKey).toString("base64"));
|
||||||
@@ -1,688 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* tslint:disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
|
||||||
* ## ##
|
|
||||||
* ## AUTHOR: acacode ##
|
|
||||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
import { customFetch } from "@httpClientAdapter";
|
|
||||||
export interface AuthForgotPasswordRequest {
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthLoginRequest {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthRegisterRequest {
|
|
||||||
email: string;
|
|
||||||
/** @minLength 6 */
|
|
||||||
password: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthResetPasswordRequest {
|
|
||||||
/** @minLength 6 */
|
|
||||||
new_password: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelPlan {
|
|
||||||
cycle?: string;
|
|
||||||
description?: string;
|
|
||||||
duration_limit?: number;
|
|
||||||
features?: string;
|
|
||||||
id?: string;
|
|
||||||
is_active?: boolean;
|
|
||||||
name?: string;
|
|
||||||
price?: number;
|
|
||||||
quality_limit?: string;
|
|
||||||
storage_limit?: number;
|
|
||||||
upload_limit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelUser {
|
|
||||||
avatar?: string;
|
|
||||||
created_at?: string;
|
|
||||||
email?: string;
|
|
||||||
google_id?: string;
|
|
||||||
id?: string;
|
|
||||||
password?: string;
|
|
||||||
plan_id?: string;
|
|
||||||
role?: string;
|
|
||||||
storage_used?: number;
|
|
||||||
updated_at?: string;
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelVideo {
|
|
||||||
created_at?: string;
|
|
||||||
description?: string;
|
|
||||||
duration?: number;
|
|
||||||
format?: string;
|
|
||||||
hls_path?: string;
|
|
||||||
hls_token?: string;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
processing_status?: string;
|
|
||||||
size?: number;
|
|
||||||
status?: string;
|
|
||||||
storage_type?: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
title?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
url?: string;
|
|
||||||
user_id?: string;
|
|
||||||
views?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentCreatePaymentRequest {
|
|
||||||
amount: number;
|
|
||||||
plan_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResponseResponse {
|
|
||||||
code?: number;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoCreateVideoRequest {
|
|
||||||
description?: string;
|
|
||||||
/** Maybe client knows, or we process later */
|
|
||||||
duration?: number;
|
|
||||||
format?: string;
|
|
||||||
size: number;
|
|
||||||
title: string;
|
|
||||||
/** The S3 Key or Full URL */
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoUploadURLRequest {
|
|
||||||
content_type: string;
|
|
||||||
filename: string;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QueryParamsType = Record<string | number, any>;
|
|
||||||
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
|
||||||
|
|
||||||
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
|
||||||
/** set parameter to `true` for call `securityWorker` for this request */
|
|
||||||
secure?: boolean;
|
|
||||||
/** request path */
|
|
||||||
path: string;
|
|
||||||
/** content type of request body */
|
|
||||||
type?: ContentType;
|
|
||||||
/** query params */
|
|
||||||
query?: QueryParamsType;
|
|
||||||
/** format of response (i.e. response.json() -> format: "json") */
|
|
||||||
format?: ResponseFormat;
|
|
||||||
/** request body */
|
|
||||||
body?: unknown;
|
|
||||||
/** base url */
|
|
||||||
baseUrl?: string;
|
|
||||||
/** request cancellation token */
|
|
||||||
cancelToken?: CancelToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RequestParams = Omit<
|
|
||||||
FullRequestParams,
|
|
||||||
"body" | "method" | "query" | "path"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ApiConfig<SecurityDataType = unknown> {
|
|
||||||
baseUrl?: string;
|
|
||||||
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
|
||||||
securityWorker?: (
|
|
||||||
securityData: SecurityDataType | null,
|
|
||||||
) => Promise<RequestParams | void> | RequestParams | void;
|
|
||||||
customFetch?: typeof fetch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
|
||||||
extends Response {
|
|
||||||
data: D;
|
|
||||||
error: E;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CancelToken = Symbol | string | number;
|
|
||||||
|
|
||||||
export enum ContentType {
|
|
||||||
Json = "application/json",
|
|
||||||
JsonApi = "application/vnd.api+json",
|
|
||||||
FormData = "multipart/form-data",
|
|
||||||
UrlEncoded = "application/x-www-form-urlencoded",
|
|
||||||
Text = "text/plain",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HttpClient<SecurityDataType = unknown> {
|
|
||||||
public baseUrl: string = "";
|
|
||||||
private securityData: SecurityDataType | null = null;
|
|
||||||
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
|
||||||
private abortControllers = new Map<CancelToken, AbortController>();
|
|
||||||
private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
|
|
||||||
fetch(...fetchParams);
|
|
||||||
|
|
||||||
private baseApiParams: RequestParams = {
|
|
||||||
credentials: "same-origin",
|
|
||||||
headers: {},
|
|
||||||
redirect: "follow",
|
|
||||||
referrerPolicy: "no-referrer",
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
|
||||||
Object.assign(this, apiConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSecurityData = (data: SecurityDataType | null) => {
|
|
||||||
this.securityData = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected encodeQueryParam(key: string, value: any) {
|
|
||||||
const encodedKey = encodeURIComponent(key);
|
|
||||||
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addQueryParam(query: QueryParamsType, key: string) {
|
|
||||||
return this.encodeQueryParam(key, query[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
|
||||||
const value = query[key];
|
|
||||||
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toQueryString(rawQuery?: QueryParamsType): string {
|
|
||||||
const query = rawQuery || {};
|
|
||||||
const keys = Object.keys(query).filter(
|
|
||||||
(key) => "undefined" !== typeof query[key],
|
|
||||||
);
|
|
||||||
return keys
|
|
||||||
.map((key) =>
|
|
||||||
Array.isArray(query[key])
|
|
||||||
? this.addArrayQueryParam(query, key)
|
|
||||||
: this.addQueryParam(query, key),
|
|
||||||
)
|
|
||||||
.join("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
|
||||||
const queryString = this.toQueryString(rawQuery);
|
|
||||||
return queryString ? `?${queryString}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
|
||||||
[ContentType.Json]: (input: any) =>
|
|
||||||
input !== null && (typeof input === "object" || typeof input === "string")
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.JsonApi]: (input: any) =>
|
|
||||||
input !== null && (typeof input === "object" || typeof input === "string")
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.Text]: (input: any) =>
|
|
||||||
input !== null && typeof input !== "string"
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.FormData]: (input: any) => {
|
|
||||||
if (input instanceof FormData) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(input || {}).reduce((formData, key) => {
|
|
||||||
const property = input[key];
|
|
||||||
formData.append(
|
|
||||||
key,
|
|
||||||
property instanceof Blob
|
|
||||||
? property
|
|
||||||
: typeof property === "object" && property !== null
|
|
||||||
? JSON.stringify(property)
|
|
||||||
: `${property}`,
|
|
||||||
);
|
|
||||||
return formData;
|
|
||||||
}, new FormData());
|
|
||||||
},
|
|
||||||
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
|
||||||
};
|
|
||||||
|
|
||||||
protected mergeRequestParams(
|
|
||||||
params1: RequestParams,
|
|
||||||
params2?: RequestParams,
|
|
||||||
): RequestParams {
|
|
||||||
return {
|
|
||||||
...this.baseApiParams,
|
|
||||||
...params1,
|
|
||||||
...(params2 || {}),
|
|
||||||
headers: {
|
|
||||||
...(this.baseApiParams.headers || {}),
|
|
||||||
...(params1.headers || {}),
|
|
||||||
...((params2 && params2.headers) || {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createAbortSignal = (
|
|
||||||
cancelToken: CancelToken,
|
|
||||||
): AbortSignal | undefined => {
|
|
||||||
if (this.abortControllers.has(cancelToken)) {
|
|
||||||
const abortController = this.abortControllers.get(cancelToken);
|
|
||||||
if (abortController) {
|
|
||||||
return abortController.signal;
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
this.abortControllers.set(cancelToken, abortController);
|
|
||||||
return abortController.signal;
|
|
||||||
};
|
|
||||||
|
|
||||||
public abortRequest = (cancelToken: CancelToken) => {
|
|
||||||
const abortController = this.abortControllers.get(cancelToken);
|
|
||||||
|
|
||||||
if (abortController) {
|
|
||||||
abortController.abort();
|
|
||||||
this.abortControllers.delete(cancelToken);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public request = async <T = any, E = any>({
|
|
||||||
body,
|
|
||||||
secure,
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
query,
|
|
||||||
format,
|
|
||||||
baseUrl,
|
|
||||||
cancelToken,
|
|
||||||
...params
|
|
||||||
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
|
||||||
const secureParams =
|
|
||||||
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
|
||||||
this.securityWorker &&
|
|
||||||
(await this.securityWorker(this.securityData))) ||
|
|
||||||
{};
|
|
||||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
|
||||||
const queryString = query && this.toQueryString(query);
|
|
||||||
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
|
||||||
const responseFormat = format || requestParams.format;
|
|
||||||
|
|
||||||
return this.customFetch(
|
|
||||||
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
|
||||||
{
|
|
||||||
...requestParams,
|
|
||||||
headers: {
|
|
||||||
...(requestParams.headers || {}),
|
|
||||||
...(type && type !== ContentType.FormData
|
|
||||||
? { "Content-Type": type }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
signal:
|
|
||||||
(cancelToken
|
|
||||||
? this.createAbortSignal(cancelToken)
|
|
||||||
: requestParams.signal) || null,
|
|
||||||
body:
|
|
||||||
typeof body === "undefined" || body === null
|
|
||||||
? null
|
|
||||||
: payloadFormatter(body)
|
|
||||||
},
|
|
||||||
).then(async (response) => {
|
|
||||||
const r = response as HttpResponse<T, E>;
|
|
||||||
r.data = null as unknown as T;
|
|
||||||
r.error = null as unknown as E;
|
|
||||||
|
|
||||||
const responseToParse = responseFormat ? response.clone() : response;
|
|
||||||
const data = !responseFormat
|
|
||||||
? r
|
|
||||||
: await responseToParse[responseFormat]()
|
|
||||||
.then((data) => {
|
|
||||||
if (r.ok) {
|
|
||||||
r.data = data;
|
|
||||||
} else {
|
|
||||||
r.error = data;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
r.error = e;
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cancelToken) {
|
|
||||||
this.abortControllers.delete(cancelToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) throw data;
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @title Stream API
|
|
||||||
* @version 1.0
|
|
||||||
* @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
|
|
||||||
* @termsOfService http://swagger.io/terms/
|
|
||||||
* @contact API Support <support@swagger.io> (http://www.swagger.io/support)
|
|
||||||
*
|
|
||||||
* This is the API server for Stream application.
|
|
||||||
*/
|
|
||||||
export class Api<
|
|
||||||
SecurityDataType extends unknown,
|
|
||||||
> extends HttpClient<SecurityDataType> {
|
|
||||||
auth = {
|
|
||||||
/**
|
|
||||||
* @description Request password reset link
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name ForgotPasswordCreate
|
|
||||||
* @summary Forgot Password
|
|
||||||
* @request POST:/auth/forgot-password
|
|
||||||
*/
|
|
||||||
forgotPasswordCreate: (
|
|
||||||
request: AuthForgotPasswordRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse, ResponseResponse>({
|
|
||||||
path: `/auth/forgot-password`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Callback for Google Login
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name GoogleCallbackList
|
|
||||||
* @summary Google Callback
|
|
||||||
* @request GET:/auth/google/callback
|
|
||||||
*/
|
|
||||||
googleCallbackList: (params: RequestParams = {}) =>
|
|
||||||
this.request<
|
|
||||||
ResponseResponse & {
|
|
||||||
data?: ModelUser;
|
|
||||||
},
|
|
||||||
ResponseResponse
|
|
||||||
>({
|
|
||||||
path: `/auth/google/callback`,
|
|
||||||
method: "GET",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Redirect to Google for Login
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name GoogleLoginList
|
|
||||||
* @summary Google Login
|
|
||||||
* @request GET:/auth/google/login
|
|
||||||
*/
|
|
||||||
googleLoginList: (params: RequestParams = {}) =>
|
|
||||||
this.request<any, void>({
|
|
||||||
path: `/auth/google/login`,
|
|
||||||
method: "GET",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Login with email and password
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name LoginCreate
|
|
||||||
* @summary Login
|
|
||||||
* @request POST:/auth/login
|
|
||||||
*/
|
|
||||||
loginCreate: (request: AuthLoginRequest, params: RequestParams = {}) =>
|
|
||||||
this.request<
|
|
||||||
ResponseResponse & {
|
|
||||||
data?: ModelUser;
|
|
||||||
},
|
|
||||||
ResponseResponse
|
|
||||||
>({
|
|
||||||
path: `/auth/login`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Logout user and clear cookies
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name LogoutCreate
|
|
||||||
* @summary Logout
|
|
||||||
* @request POST:/auth/logout
|
|
||||||
*/
|
|
||||||
logoutCreate: (params: RequestParams = {}) =>
|
|
||||||
this.request<ResponseResponse, any>({
|
|
||||||
path: `/auth/logout`,
|
|
||||||
method: "POST",
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Register a new user
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name RegisterCreate
|
|
||||||
* @summary Register
|
|
||||||
* @request POST:/auth/register
|
|
||||||
*/
|
|
||||||
registerCreate: (
|
|
||||||
request: AuthRegisterRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse, ResponseResponse>({
|
|
||||||
path: `/auth/register`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Reset password using token
|
|
||||||
*
|
|
||||||
* @tags auth
|
|
||||||
* @name ResetPasswordCreate
|
|
||||||
* @summary Reset Password
|
|
||||||
* @request POST:/auth/reset-password
|
|
||||||
*/
|
|
||||||
resetPasswordCreate: (
|
|
||||||
request: AuthResetPasswordRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse, ResponseResponse>({
|
|
||||||
path: `/auth/reset-password`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
payments = {
|
|
||||||
/**
|
|
||||||
* @description Create a new payment
|
|
||||||
*
|
|
||||||
* @tags payment
|
|
||||||
* @name PaymentsCreate
|
|
||||||
* @summary Create Payment
|
|
||||||
* @request POST:/payments
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
paymentsCreate: (
|
|
||||||
request: PaymentCreatePaymentRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse, ResponseResponse>({
|
|
||||||
path: `/payments`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
secure: true,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
plans = {
|
|
||||||
/**
|
|
||||||
* @description Get all active plans
|
|
||||||
*
|
|
||||||
* @tags plan
|
|
||||||
* @name PlansList
|
|
||||||
* @summary List Plans
|
|
||||||
* @request GET:/plans
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
plansList: (params: RequestParams = {}) =>
|
|
||||||
this.request<
|
|
||||||
ResponseResponse & {
|
|
||||||
data: {
|
|
||||||
plans: ModelPlan[];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ResponseResponse
|
|
||||||
>({
|
|
||||||
path: `/plans`,
|
|
||||||
method: "GET",
|
|
||||||
secure: true,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
videos = {
|
|
||||||
/**
|
|
||||||
* @description Get paginated videos
|
|
||||||
*
|
|
||||||
* @tags video
|
|
||||||
* @name VideosList
|
|
||||||
* @summary List Videos
|
|
||||||
* @request GET:/videos
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
videosList: (
|
|
||||||
query?: {
|
|
||||||
/**
|
|
||||||
* Page number
|
|
||||||
* @default 1
|
|
||||||
*/
|
|
||||||
page?: number;
|
|
||||||
/**
|
|
||||||
* Page size
|
|
||||||
* @default 10
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
},
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse & {
|
|
||||||
data: {
|
|
||||||
limit: number;
|
|
||||||
page: number;
|
|
||||||
total: number;
|
|
||||||
videos: ModelVideo[];
|
|
||||||
}
|
|
||||||
}, ResponseResponse>({
|
|
||||||
path: `/videos`,
|
|
||||||
method: "GET",
|
|
||||||
query: query,
|
|
||||||
secure: true,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Create video record after upload
|
|
||||||
*
|
|
||||||
* @tags video
|
|
||||||
* @name VideosCreate
|
|
||||||
* @summary Create Video
|
|
||||||
* @request POST:/videos
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
videosCreate: (
|
|
||||||
request: VideoCreateVideoRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<
|
|
||||||
ResponseResponse & {
|
|
||||||
data?: ModelVideo;
|
|
||||||
},
|
|
||||||
ResponseResponse
|
|
||||||
>({
|
|
||||||
path: `/videos`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
secure: true,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Generate presigned URL for video upload
|
|
||||||
*
|
|
||||||
* @tags video
|
|
||||||
* @name UploadUrlCreate
|
|
||||||
* @summary Get Upload URL
|
|
||||||
* @request POST:/videos/upload-url
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
uploadUrlCreate: (
|
|
||||||
request: VideoUploadURLRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
this.request<ResponseResponse, ResponseResponse>({
|
|
||||||
path: `/videos/upload-url`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
secure: true,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Get video details by ID
|
|
||||||
*
|
|
||||||
* @tags video
|
|
||||||
* @name VideosDetail
|
|
||||||
* @summary Get Video
|
|
||||||
* @request GET:/videos/{id}
|
|
||||||
* @secure
|
|
||||||
*/
|
|
||||||
videosDetail: (id: string, params: RequestParams = {}) =>
|
|
||||||
this.request<
|
|
||||||
ResponseResponse & {
|
|
||||||
data?: ModelVideo;
|
|
||||||
},
|
|
||||||
ResponseResponse
|
|
||||||
>({
|
|
||||||
path: `/videos/${id}`,
|
|
||||||
method: "GET",
|
|
||||||
secure: true,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const client = new Api({
|
|
||||||
baseUrl: 'r',
|
|
||||||
// baseUrl: 'https://api.pipic.fun',
|
|
||||||
customFetch
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,78 @@
|
|||||||
export const customFetch = (url: string, options: RequestInit) => {
|
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
|
||||||
return fetch(url, {
|
import { Result } from "@hiogawa/utils";
|
||||||
...options,
|
|
||||||
credentials: "include",
|
const GET_PAYLOAD_PARAM = "payload";
|
||||||
});
|
|
||||||
|
export function httpClientAdapter(opts: {
|
||||||
|
url: string;
|
||||||
|
pathsForGET?: string[];
|
||||||
|
JSON?: Partial<JsonTransformer>;
|
||||||
|
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
||||||
|
}): TinyRpcClientAdapter {
|
||||||
|
const JSON: JsonTransformer = {
|
||||||
|
parse: globalThis.JSON.parse,
|
||||||
|
stringify: globalThis.JSON.stringify as JsonTransformer["stringify"],
|
||||||
|
...opts.JSON,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
send: async (data) => {
|
||||||
|
const url = [opts.url, data.path].join("/");
|
||||||
|
const extraHeaders = opts.headers ? await opts.headers() : {};
|
||||||
|
const payload = JSON.stringify(data.args, (headerObj) => {
|
||||||
|
if (headerObj) {
|
||||||
|
Object.assign(extraHeaders, headerObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const method = opts.pathsForGET?.includes(data.path)
|
||||||
|
? "GET"
|
||||||
|
: "POST";
|
||||||
|
|
||||||
|
let req: Request;
|
||||||
|
if (method === "GET") {
|
||||||
|
req = new Request(
|
||||||
|
url +
|
||||||
|
"?" +
|
||||||
|
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }),
|
||||||
|
{
|
||||||
|
headers: extraHeaders
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
req = new Request(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
...extraHeaders
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let res: Response;
|
||||||
|
|
||||||
|
res = await fetch(req);
|
||||||
|
if (!res.ok) {
|
||||||
|
// throw new Error(`HTTP error: ${res.status}`);
|
||||||
|
throw new Error(
|
||||||
|
JSON.stringify({
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
data: { message: await res.text() },
|
||||||
|
internal: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// throw TinyRpcError.deserialize(res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Result<unknown, unknown> = JSON.parse(
|
||||||
|
await res.text(),
|
||||||
|
() => Object.fromEntries((res.headers as any).entries() ?? [])
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
throw TinyRpcError.deserialize(result.value);
|
||||||
|
}
|
||||||
|
return result.value;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,88 @@
|
|||||||
|
import { TinyRpcClientAdapter, TinyRpcError } from "@hiogawa/tiny-rpc";
|
||||||
|
import { Result } from "@hiogawa/utils";
|
||||||
import { tryGetContext } from "hono/context-storage";
|
import { tryGetContext } from "hono/context-storage";
|
||||||
|
|
||||||
|
const GET_PAYLOAD_PARAM = "payload";
|
||||||
export const baseAPIURL = "https://api.pipic.fun";
|
export const baseAPIURL = "https://api.pipic.fun";
|
||||||
export const customFetch = (url: string, options: RequestInit) => {
|
|
||||||
options.credentials = "include";
|
|
||||||
const c = tryGetContext<any>();
|
|
||||||
if (!c) {
|
|
||||||
throw new Error("Hono context not found in SSR");
|
|
||||||
}
|
|
||||||
// Merge headers properly - keep original options.headers and add request headers
|
|
||||||
const reqHeaders = new Headers(c.req.header());
|
|
||||||
// Remove headers that shouldn't be forwarded
|
|
||||||
reqHeaders.delete("host");
|
|
||||||
reqHeaders.delete("connection");
|
|
||||||
|
|
||||||
const mergedHeaders: Record<string, string> = {};
|
export function httpClientAdapter(opts: {
|
||||||
reqHeaders.forEach((value, key) => {
|
url: string;
|
||||||
mergedHeaders[key] = value;
|
pathsForGET?: string[];
|
||||||
});
|
JSON?: Partial<JsonTransformer>;
|
||||||
options.headers = {
|
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
||||||
...mergedHeaders,
|
}): TinyRpcClientAdapter {
|
||||||
...(options.headers as Record<string, string>),
|
const JSON: JsonTransformer = {
|
||||||
|
parse: globalThis.JSON.parse,
|
||||||
|
stringify: globalThis.JSON.stringify as JsonTransformer["stringify"],
|
||||||
|
...opts.JSON,
|
||||||
};
|
};
|
||||||
|
return {
|
||||||
|
send: async (data) => {
|
||||||
|
const url = [opts.url, data.path].join("/");
|
||||||
|
const extraHeaders = opts.headers ? await opts.headers() : {};
|
||||||
|
const payload = JSON.stringify(data.args, (headerObj) => {
|
||||||
|
if (headerObj) {
|
||||||
|
Object.assign(extraHeaders, headerObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const method = opts.pathsForGET?.includes(data.path)
|
||||||
|
? "GET"
|
||||||
|
: "POST";
|
||||||
|
let req: Request;
|
||||||
|
if (method === "GET") {
|
||||||
|
req = new Request(
|
||||||
|
url +
|
||||||
|
"?" +
|
||||||
|
new URLSearchParams({ [GET_PAYLOAD_PARAM]: payload }),
|
||||||
|
{
|
||||||
|
headers: extraHeaders
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
req = new Request(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
...extraHeaders,
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let res: Response;
|
||||||
|
if (import.meta.env.SSR) {
|
||||||
|
const c = tryGetContext<any>();
|
||||||
|
if (!c) {
|
||||||
|
throw new Error("Hono context not found in SSR");
|
||||||
|
}
|
||||||
|
Object.entries(c.req.header()).forEach(([k, v]) => {
|
||||||
|
req.headers.append(k, v);
|
||||||
|
});
|
||||||
|
res = await c.get("fetch")(req);
|
||||||
|
} else {
|
||||||
|
res = await fetch(req);
|
||||||
|
}
|
||||||
|
|
||||||
const apiUrl = [baseAPIURL, url.replace(/^r/, "")].join("");
|
if (!res.ok) {
|
||||||
return fetch(apiUrl, options).then(async (res) => {
|
// throw new Error(`HTTP error: ${res.status}`);
|
||||||
res.headers.getSetCookie()?.forEach((cookie) => {
|
throw new Error(
|
||||||
c.header("Set-Cookie", cookie);
|
JSON.stringify({
|
||||||
});
|
status: res.status,
|
||||||
return res;
|
statusText: res.statusText,
|
||||||
});
|
data: { message: await res.text() },
|
||||||
};
|
internal: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// throw TinyRpcError.deserialize(res.status);
|
||||||
|
}
|
||||||
|
const result: Result<unknown, unknown> = JSON.parse(
|
||||||
|
await res.text(),
|
||||||
|
() => Object.fromEntries((res.headers as any).entries() ?? [])
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
throw TinyRpcError.deserialize(result.value);
|
||||||
|
}
|
||||||
|
return result.value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
39
src/api/rpcclient.ts
Normal file
39
src/api/rpcclient.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { RpcRoutes } from "@/server/routes/rpc";
|
||||||
|
import { proxyTinyRpc } from "@hiogawa/tiny-rpc";
|
||||||
|
import { httpClientAdapter } from "@httpClientAdapter";
|
||||||
|
|
||||||
|
const endpoint = "/rpc";
|
||||||
|
const publicEndpoint = "/rpc-public";
|
||||||
|
const url = import.meta.env.SSR ? "http://localhost" : "";
|
||||||
|
const publicMethods = ["login", "register", "forgotPassword", "resetPassword", "getGoogleLoginUrl"];
|
||||||
|
// src/client/trpc-client-transformer.ts
|
||||||
|
import {
|
||||||
|
clientJSON
|
||||||
|
} from "@/shared/secure-json-transformer";
|
||||||
|
|
||||||
|
|
||||||
|
// export function createTrpcClientTransformer(cfg: ServerPublicKeyConfig) {
|
||||||
|
// return {
|
||||||
|
// input: ,
|
||||||
|
// output: superjson,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// const secureConfig = await fetch("/trpc-secure-config").then((r) => r.json());
|
||||||
|
export const client = proxyTinyRpc<RpcRoutes>({
|
||||||
|
adapter: {
|
||||||
|
send: async (data) => {
|
||||||
|
const targetEndpoint = publicMethods.includes(data.path) ? publicEndpoint : endpoint;
|
||||||
|
return await httpClientAdapter({
|
||||||
|
url: `${url}${targetEndpoint}`,
|
||||||
|
pathsForGET: ["health"],
|
||||||
|
JSON: {
|
||||||
|
// parse: clientJSON.parse,
|
||||||
|
parse: (v, fn) => JSON.parse(v),
|
||||||
|
// stringify: clientJSON.stringify,
|
||||||
|
stringify: (v, fn) => JSON.stringify(v),
|
||||||
|
},
|
||||||
|
headers: () => Promise.resolve({})
|
||||||
|
}).send(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -3,14 +3,20 @@ import 'uno.css';
|
|||||||
import PiniaSharedState from './lib/PiniaSharedState';
|
import PiniaSharedState from './lib/PiniaSharedState';
|
||||||
import { createApp } from './main';
|
import { createApp } from './main';
|
||||||
|
|
||||||
|
const readAppData = () => {
|
||||||
|
return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
const { app, router, queryCache, pinia } = createApp();
|
const appData = readAppData();
|
||||||
pinia.use(PiniaSharedState({enable: true, initialize: true}));
|
const { app, router, queryCache, pinia } = await createApp(appData.$locale);
|
||||||
hydrateQueryCache(queryCache, (window as any).$colada || {});
|
pinia.use(PiniaSharedState({ enable: true, initialize: true }));
|
||||||
router.isReady().then(() => {
|
hydrateQueryCache(queryCache, appData.$colada || {});
|
||||||
app.mount('body', true)
|
|
||||||
})
|
await router.isReady();
|
||||||
|
app.mount('body', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
render().catch((error) => {
|
render().catch((error) => {
|
||||||
console.error('Error during app initialization:', error)
|
console.error('Error during app initialization:', error);
|
||||||
})
|
});
|
||||||
|
|||||||
23
src/components/AppTopLoadingBar.vue
Normal file
23
src/components/AppTopLoadingBar.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouteLoading } from '@/composables/useRouteLoading';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const { visible, progress } = useRouteLoading()
|
||||||
|
|
||||||
|
const barStyle = computed(() => ({
|
||||||
|
transform: `scaleX(${progress.value / 100})`,
|
||||||
|
opacity: visible.value ? '1' : '0',
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed inset-x-0 top-0 z-[9999] h-0.75"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full origin-left rounded-r-full bg-primary/50 transition-[transform,opacity] duration-200 ease-out"
|
||||||
|
:style="barStyle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import Upload from "@/routes/upload/Upload.vue";
|
||||||
import DashboardNav from "./DashboardNav.vue";
|
import DashboardNav from "./DashboardNav.vue";
|
||||||
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
||||||
import Upload from "@/routes/upload/Upload.vue";
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DashboardNav />
|
<DashboardNav />
|
||||||
<main class="flex flex-1 flex-col transition-all duration-300 ease-in-out bg-page md:ps-18">
|
<main class="flex flex-1 flex-col transition-all duration-300 ease-in-out bg-white md:ps-18">
|
||||||
<div class=":uno: flex-1 overflow-auto p-4 bg-page rounded-lg md:(mr-2 mb-2) min-h-[calc(100vh-8rem)]">
|
<div class=":uno: flex-1 overflow-auto p-4 bg-white rounded-lg md:(mr-2 mb-2) min-h-[calc(100vh-8rem)]">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<Transition enter-active-class="transition-all duration-300 ease-in-out"
|
<Transition enter-active-class="transition-all duration-300 ease-in-out"
|
||||||
enter-from-class="opacity-0 transform translate-y-4"
|
enter-from-class="opacity-0 transform translate-y-4"
|
||||||
|
|||||||
@@ -1,51 +1,69 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import Bell from "@/components/icons/Bell.vue";
|
import Bell from "@/components/icons/Bell.vue";
|
||||||
import Home from "@/components/icons/Home.vue";
|
import Home from "@/components/icons/Home.vue";
|
||||||
import Video from "@/components/icons/Video.vue";
|
import LayoutDashboard from "@/components/icons/LayoutDashboard.vue";
|
||||||
import SettingsIcon from "@/components/icons/SettingsIcon.vue";
|
import SettingsIcon from "@/components/icons/SettingsIcon.vue";
|
||||||
// import Upload from "@/components/icons/Upload.vue";
|
import Video from "@/components/icons/Video.vue";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { createStaticVNode, ref } from "vue";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { useTranslation } from "i18next-vue";
|
||||||
|
import { computed, createStaticVNode, ref } from "vue";
|
||||||
import NotificationDrawer from "./NotificationDrawer.vue";
|
import NotificationDrawer from "./NotificationDrawer.vue";
|
||||||
|
|
||||||
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
|
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
|
||||||
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
|
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
|
||||||
const notificationPopover = ref<InstanceType<typeof NotificationDrawer>>();
|
const notificationPopover = ref<InstanceType<typeof NotificationDrawer>>();
|
||||||
const isNotificationOpen = ref(false);
|
const isNotificationOpen = ref(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
const isAdmin = computed(() => String(auth.user?.role || "").toLowerCase() === "admin");
|
||||||
|
|
||||||
const handleNotificationClick = (event: Event) => {
|
const handleNotificationClick = (event: Event) => {
|
||||||
notificationPopover.value?.toggle(event);
|
notificationPopover.value?.toggle(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
const links = [
|
const links = computed<Record<string, any>>(() => {
|
||||||
{ href: "/#home", label: "app", icon: homeHoist, type: "btn", className },
|
const baseLinks = [
|
||||||
{ href: "/", label: "Overview", icon: Home, type: "a", className },
|
{ href: "/#home", label: "app", icon: homeHoist, action: () => {}, className },
|
||||||
// { href: "/upload", label: "Upload", icon: Upload, type: "a", className },
|
{ href: "/", label: t("nav.overview"), icon: Home, action: null, className },
|
||||||
{ href: "/videos", label: "Videos", icon: Video, type: "a", className },
|
{ href: "/videos", label: t("nav.videos"), icon: Video, action: null, className },
|
||||||
{ href: "/notification", label: "Notification", icon: Bell, type: "btn", className, action: handleNotificationClick, isActive: isNotificationOpen },
|
{
|
||||||
{ href: "/settings", label: "Settings", icon: SettingsIcon, type: "a", className },
|
href: "/notification",
|
||||||
];
|
label: t("nav.notification"),
|
||||||
|
icon: Bell,
|
||||||
|
className,
|
||||||
//v-tooltip="i.label"
|
action: handleNotificationClick,
|
||||||
|
isActive: isNotificationOpen,
|
||||||
|
},
|
||||||
|
{ href: "/settings", label: t("nav.settings"), icon: SettingsIcon, action: null, className },
|
||||||
|
] as const;
|
||||||
|
return baseLinks;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-muted transition-all duration-300 ease-in-out w-18 items-center">
|
class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-header transition-all duration-300 ease-in-out w-18 items-center border-r border-border text-foreground/60"
|
||||||
|
>
|
||||||
<template v-for="i in links" :key="i.label">
|
<template v-for="i in links" :key="i.href">
|
||||||
<component :name="i.label" :is="i.type === 'a' ? 'router-link' : 'div'"
|
<component
|
||||||
v-bind="i.type === 'a' ? { to: i.href } : {}"
|
:name="i.label"
|
||||||
@click="i.action && i.action($event)"
|
:is="i.action ? 'div' : 'router-link'"
|
||||||
:class="cn(
|
v-bind="i.action ? {} : { to: i.href }"
|
||||||
i.className,
|
@click="i.action && i.action($event)"
|
||||||
($route.path === i.href || $route.path.startsWith(i.href+'/') || i.isActive?.value) && 'bg-primary/15'
|
:class="cn(
|
||||||
)">
|
i.className,
|
||||||
<component :is="i.icon" class="w-6 h-6 shrink-0"
|
($route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value) && 'bg-primary/15',
|
||||||
:filled="$route.path === i.href || $route.path.startsWith(i.href+'/') || i.isActive?.value" />
|
)"
|
||||||
</component>
|
>
|
||||||
</template>
|
<component
|
||||||
</header>
|
:is="i.icon"
|
||||||
<NotificationDrawer ref="notificationPopover" @change="(val) => isNotificationOpen = val" />
|
class="w-6 h-6 shrink-0"
|
||||||
|
:filled="$route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value"
|
||||||
|
/>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
</header>
|
||||||
|
<NotificationDrawer ref="notificationPopover" @change="(val) => (isNotificationOpen = val)" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { useUploadQueue } from '@/composables/useUploadQueue';
|
|||||||
import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue';
|
import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue';
|
||||||
import { useUIState } from '@/stores/uiState';
|
import { useUIState } from '@/stores/uiState';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { items, completeCount, pendingCount, startQueue, removeItem, cancelItem, removeAll } = useUploadQueue();
|
const { items, completeCount, pendingCount, startQueue, removeItem, cancelItem, removeAll } = useUploadQueue();
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
|
|
||||||
@@ -28,13 +30,13 @@ const isAllDone = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
if (isAllDone.value) return 'All done';
|
if (isAllDone.value) return t('upload.indicator.allDone');
|
||||||
if (isUploading.value) {
|
if (isUploading.value) {
|
||||||
const count = items.value.filter(i => i.status === 'uploading' || i.status === 'fetching').length;
|
const count = items.value.filter(i => i.status === 'uploading' || i.status === 'fetching').length;
|
||||||
return `Uploading ${count} file${count !== 1 ? 's' : ''}...`;
|
return t('upload.indicator.uploading', { count });
|
||||||
}
|
}
|
||||||
if (pendingCount.value > 0) return `${pendingCount.value} file${pendingCount.value !== 1 ? 's' : ''} waiting`;
|
if (pendingCount.value > 0) return t('upload.indicator.waiting', { count: pendingCount.value });
|
||||||
return 'Processing...';
|
return t('upload.queueItem.status.processing');
|
||||||
});
|
});
|
||||||
const isDoneWithErrors = computed(() =>
|
const isDoneWithErrors = computed(() =>
|
||||||
isAllDone.value &&
|
isAllDone.value &&
|
||||||
@@ -87,7 +89,7 @@ watch(isAllDone, (newItems) => {
|
|||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-semibold leading-tight truncate">{{ statusText }}</p>
|
<p class="text-sm font-semibold leading-tight truncate">{{ statusText }}</p>
|
||||||
<p class="text-xs text-slate-400 leading-tight mt-0.5">
|
<p class="text-xs text-slate-400 leading-tight mt-0.5">
|
||||||
{{ completeCount }} of {{ items.length }} complete
|
{{ t('upload.indicator.completeProgress', { complete: completeCount, total: items.length }) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,17 +102,17 @@ watch(isAllDone, (newItems) => {
|
|||||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polygon points="5 3 19 12 5 21 5 3" />
|
<polygon points="5 3 19 12 5 21 5 3" />
|
||||||
</svg>
|
</svg>
|
||||||
Start
|
{{ t('upload.indicator.start') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="isDoneWithErrors" @click.stop="doneUpload"
|
<button v-else-if="isDoneWithErrors" @click.stop="doneUpload"
|
||||||
class="flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 bg-green-500 hover:bg-green-500/80 text-white rounded-lg transition-all">
|
class="flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 bg-green-500 hover:bg-green-500/80 text-white rounded-lg transition-all">
|
||||||
View Videos
|
{{ t('upload.indicator.viewVideos') }}
|
||||||
</button>
|
</button>
|
||||||
<!-- Clear queue -->
|
<!-- Clear queue -->
|
||||||
<!-- Add more files -->
|
<!-- Add more files -->
|
||||||
<button @click.stop="uiState.uploadDialogVisible = true"
|
<button @click.stop="uiState.uploadDialogVisible = true"
|
||||||
class="w-7 h-7 flex items-center justify-center text-slate-400 hover:text-white hover:bg-white/10 rounded-lg transition-all"
|
class="w-7 h-7 flex items-center justify-center text-slate-400 hover:text-white hover:bg-white/10 rounded-lg transition-all"
|
||||||
title="Add more files">
|
:title="t('upload.indicator.addMoreFiles')">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M5 12h14" />
|
<path d="M5 12h14" />
|
||||||
|
|||||||
@@ -1,122 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NotificationItem from '@/routes/notification/components/NotificationItem.vue';
|
import NotificationItem from '@/routes/notification/components/NotificationItem.vue';
|
||||||
|
import { useNotifications } from '@/composables/useNotifications';
|
||||||
import { onClickOutside } from '@vueuse/core';
|
import { onClickOutside } from '@vueuse/core';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
// Ensure client-side only rendering to avoid hydration mismatch
|
|
||||||
const isMounted = ref(false);
|
const isMounted = ref(false);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isMounted.value = true;
|
isMounted.value = true;
|
||||||
|
void notificationStore.fetchNotifications();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit event when visibility changes
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
type: NotificationType;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
time: string;
|
|
||||||
read: boolean;
|
|
||||||
actionUrl?: string;
|
|
||||||
actionLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const drawerRef = ref(null);
|
const drawerRef = ref(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const notificationStore = useNotifications();
|
||||||
|
|
||||||
// Mock notifications data
|
const unreadCount = computed(() => notificationStore.unreadCount.value);
|
||||||
const notifications = ref<Notification[]>([
|
const mutableNotifications = computed(() => notificationStore.notifications.value.slice(0, 8));
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'video',
|
|
||||||
title: 'Video processing complete',
|
|
||||||
message: 'Your video "Summer Vacation 2024" has been successfully processed.',
|
|
||||||
time: '2 min ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/video',
|
|
||||||
actionLabel: 'View'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'payment',
|
|
||||||
title: 'Payment successful',
|
|
||||||
message: 'Your subscription to Pro Plan has been renewed successfully.',
|
|
||||||
time: '1 hour ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/payments-and-plans',
|
|
||||||
actionLabel: 'Receipt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Storage almost full',
|
|
||||||
message: 'You have used 85% of your storage quota.',
|
|
||||||
time: '3 hours ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/payments-and-plans',
|
|
||||||
actionLabel: 'Upgrade'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
type: 'success',
|
|
||||||
title: 'Upload successful',
|
|
||||||
message: 'Your video "Product Demo v2" has been uploaded.',
|
|
||||||
time: '1 day ago',
|
|
||||||
read: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const unreadCount = computed(() => notifications.value.filter(n => !n.read).length);
|
|
||||||
|
|
||||||
const toggle = (event?: Event) => {
|
const toggle = (event?: Event) => {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
// Prevent event propagation to avoid immediate closure by onClickOutside
|
|
||||||
if (event) {
|
|
||||||
// We don't stop propagation here to let other listeners work,
|
|
||||||
// but we might need to ignore the trigger element in onClickOutside
|
|
||||||
// However, since the trigger is outside this component, simple toggle logic works
|
|
||||||
// if we use a small delay or ignore ref.
|
|
||||||
// Best approach: "toggle" usually comes from a button click.
|
|
||||||
}
|
|
||||||
visible.value = !visible.value;
|
visible.value = !visible.value;
|
||||||
console.log(visible.value);
|
if (visible.value && !notificationStore.loaded.value) {
|
||||||
|
void notificationStore.fetchNotifications();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle click outside
|
onClickOutside(drawerRef, () => {
|
||||||
onClickOutside(drawerRef, (event) => {
|
|
||||||
// We can just set visible to false.
|
|
||||||
// Note: If the toggle button is clicked, it might toggle it back on immediately
|
|
||||||
// if the click event propagates.
|
|
||||||
// The user calls `toggle` from the parent's button click handler.
|
|
||||||
// If that button is outside `drawerRef` (which it is), this will fire.
|
|
||||||
// To avoid conflict, we usually check if the target is the trigger.
|
|
||||||
// But we don't have access to the trigger ref here.
|
|
||||||
// A common workaround is to use `ignore` option if we had the ref,
|
|
||||||
// or relying on the fact that if this fires, it sets specific state to false.
|
|
||||||
// If the button click then fires `toggle`, it might set it true again.
|
|
||||||
// Optimization: check if visible is true before closing.
|
|
||||||
if (visible.value) {
|
if (visible.value) {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
ignore: ['[name="Notification"]'] // Assuming the trigger button has this class or we can suggest adding a class to the trigger
|
ignore: ['[name="Notification"]']
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleMarkRead = (id: string) => {
|
const handleMarkRead = async (id: string) => {
|
||||||
const notification = notifications.value.find(n => n.id === id);
|
await notificationStore.markRead(id);
|
||||||
if (notification) notification.read = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
notifications.value = notifications.value.filter(n => n.id !== id);
|
await notificationStore.deleteNotification(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = () => {
|
const handleMarkAllRead = async () => {
|
||||||
notifications.value.forEach(n => n.read = true);
|
await notificationStore.markAllRead();
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(visible, (val) => {
|
watch(visible, (val) => {
|
||||||
@@ -134,10 +63,9 @@ defineExpose({ toggle });
|
|||||||
leave-to-class="opacity-0 -translate-x-4">
|
leave-to-class="opacity-0 -translate-x-4">
|
||||||
<div v-if="visible" ref="drawerRef"
|
<div v-if="visible" ref="drawerRef"
|
||||||
class="fixed top-0 left-[80px] bottom-0 w-[380px] bg-white rounded-2xl border border-gray-300 p-3 z-50 flex flex-col shadow-lg my-3">
|
class="fixed top-0 left-[80px] bottom-0 w-[380px] bg-white rounded-2xl border border-gray-300 p-3 z-50 flex flex-col shadow-lg my-3">
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between p-4">
|
<div class="flex items-center justify-between p-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h3 class="font-semibold text-gray-900">Notifications</h3>
|
<h3 class="font-semibold text-gray-900">{{ t('notification.title') }}</h3>
|
||||||
<span v-if="unreadCount > 0"
|
<span v-if="unreadCount > 0"
|
||||||
class="px-2 py-0.5 text-xs font-medium bg-primary text-white rounded-full">
|
class="px-2 py-0.5 text-xs font-medium bg-primary text-white rounded-full">
|
||||||
{{ unreadCount }}
|
{{ unreadCount }}
|
||||||
@@ -145,49 +73,44 @@ defineExpose({ toggle });
|
|||||||
</div>
|
</div>
|
||||||
<button v-if="unreadCount > 0" @click="handleMarkAllRead"
|
<button v-if="unreadCount > 0" @click="handleMarkAllRead"
|
||||||
class="text-sm text-primary hover:underline font-medium">
|
class="text-sm text-primary hover:underline font-medium">
|
||||||
Mark all read
|
{{ t('notification.actions.markAllRead') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notification List -->
|
|
||||||
<div class="flex flex-col flex-1 overflow-y-auto gap-2">
|
<div class="flex flex-col flex-1 overflow-y-auto gap-2">
|
||||||
<template v-if="notifications.length > 0">
|
<template v-if="notificationStore.loading.value">
|
||||||
<div v-for="notification in notifications" :key="notification.id"
|
<div v-for="i in 4" :key="i" class="p-4 rounded-xl border border-gray-200 animate-pulse">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-gray-200"></div>
|
||||||
|
<div class="flex-1 space-y-2">
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
|
||||||
|
<div class="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="mutableNotifications.length > 0">
|
||||||
|
<div v-for="notification in mutableNotifications" :key="notification.id"
|
||||||
class="border-b border-gray-50 last:border-0">
|
class="border-b border-gray-50 last:border-0">
|
||||||
<NotificationItem :notification="notification" @mark-read="handleMarkRead"
|
<NotificationItem :notification="notification" @mark-read="handleMarkRead"
|
||||||
@delete="handleDelete" isDrawer />
|
@delete="handleDelete" isDrawer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div v-else class="py-12 text-center">
|
<div v-else class="py-12 text-center">
|
||||||
<span class="i-lucide-bell-off w-12 h-12 text-gray-300 mx-auto block mb-3"></span>
|
<span class="i-lucide-bell-off w-12 h-12 text-gray-300 mx-auto block mb-3"></span>
|
||||||
<p class="text-gray-500 text-sm">No notifications</p>
|
<p class="text-gray-500 text-sm">{{ t('notification.empty.title') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<div v-if="mutableNotifications.length > 0" class="p-3 border-t border-gray-100 bg-gray-50/50">
|
||||||
<div v-if="notifications.length > 0" class="p-3 border-t border-gray-100 bg-gray-50/50">
|
|
||||||
<router-link to="/notification"
|
<router-link to="/notification"
|
||||||
class="block w-full text-center text-sm text-primary font-medium hover:underline"
|
class="block w-full text-center text-sm text-primary font-medium hover:underline"
|
||||||
@click="visible = false">
|
@click="visible = false">
|
||||||
View all notifications
|
{{ t('notification.actions.viewAll') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <style>
|
|
||||||
.notification-popover {
|
|
||||||
border-radius: 16px !important;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12) !important;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-popover .p-popover-content {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
</style> -->
|
|
||||||
|
|||||||
67
src/components/OfflineOverlay.vue
Normal file
67
src/components/OfflineOverlay.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AppButton from '@/components/ui/AppButton.vue'
|
||||||
|
import { useNetworkStatus } from '@/composables/useNetworkStatus'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
|
import { onBeforeUnmount, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isOffline, startListening, stopListening } = useNetworkStatus()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startListening()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopListening()
|
||||||
|
})
|
||||||
|
|
||||||
|
function reloadPage() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isOffline"
|
||||||
|
class="fixed inset-0 z-[10000] flex items-center justify-center bg-slate-950/80 px-6 backdrop-blur-sm"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-md rounded-2xl border border-border bg-white p-8 text-center shadow-2xl">
|
||||||
|
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-danger/10 text-danger">
|
||||||
|
<svg
|
||||||
|
class="h-8 w-8"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M2 8.82a15 15 0 0 1 20 0" />
|
||||||
|
<path d="M5 12.86a10 10 0 0 1 14 0" />
|
||||||
|
<path d="M8.5 16.43a5 5 0 0 1 7 0" />
|
||||||
|
<path d="M12 20h.01" />
|
||||||
|
<path d="M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-foreground">
|
||||||
|
{{ t('network.offline.title') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mt-3 text-sm leading-6 text-foreground/70">
|
||||||
|
{{ t('network.offline.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-center">
|
||||||
|
<AppButton @click="reloadPage">
|
||||||
|
{{ t('network.offline.action') }}
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view/>
|
<ClientOnly>
|
||||||
</template>
|
<AppTopLoadingBar />
|
||||||
|
<OfflineOverlay />
|
||||||
|
</ClientOnly>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ClientOnly from '@/components/ClientOnly';
|
||||||
|
import AppTopLoadingBar from '@/components/AppTopLoadingBar.vue'
|
||||||
|
import OfflineOverlay from '@/components/OfflineOverlay.vue'
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
||||||
type Size = 'sm' | 'md';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
variant?: Variant;
|
|
||||||
size?: Size;
|
|
||||||
loading?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
type?: 'button' | 'submit' | 'reset';
|
|
||||||
}>(), {
|
|
||||||
variant: 'primary',
|
|
||||||
size: 'md',
|
|
||||||
loading: false,
|
|
||||||
disabled: false,
|
|
||||||
type: 'button',
|
|
||||||
});
|
|
||||||
|
|
||||||
const baseClass = 'inline-flex items-center justify-center gap-2 rounded-md font-medium transition-all press-animated select-none';
|
|
||||||
|
|
||||||
const sizeClass = computed(() => {
|
|
||||||
switch (props.size) {
|
|
||||||
case 'sm':
|
|
||||||
return 'px-3 py-1.5 text-sm';
|
|
||||||
case 'md':
|
|
||||||
default:
|
|
||||||
return 'px-4 py-2 text-sm';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const variantClass = computed(() => {
|
|
||||||
switch (props.variant) {
|
|
||||||
case 'secondary':
|
|
||||||
return 'bg-muted/50 text-foreground hover:bg-muted border border-border';
|
|
||||||
case 'danger':
|
|
||||||
return 'bg-danger text-white hover:bg-danger/90';
|
|
||||||
case 'ghost':
|
|
||||||
return 'bg-transparent text-foreground/70 hover:text-foreground hover:bg-muted/50';
|
|
||||||
case 'primary':
|
|
||||||
default:
|
|
||||||
return 'bg-primary text-white hover:bg-primary/90';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabledClass = computed(() => (props.disabled || props.loading) ? 'opacity-60 cursor-not-allowed' : '');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
:type="type"
|
|
||||||
:disabled="disabled || loading"
|
|
||||||
:class="cn(baseClass, sizeClass, variantClass, disabledClass)"
|
|
||||||
>
|
|
||||||
<span v-if="loading" class="inline-flex items-center" aria-hidden="true">
|
|
||||||
<svg class="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<slot name="icon" />
|
|
||||||
<slot />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
ariaLabel?: string;
|
|
||||||
}>(), {
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: boolean): void;
|
|
||||||
(e: 'change', value: boolean): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
if (props.disabled) return;
|
|
||||||
const next = !props.modelValue;
|
|
||||||
emit('update:modelValue', next);
|
|
||||||
emit('change', next);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
:aria-checked="modelValue"
|
|
||||||
:aria-label="ariaLabel"
|
|
||||||
:disabled="disabled"
|
|
||||||
@click="toggle"
|
|
||||||
:class="cn(
|
|
||||||
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
|
||||||
disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
|
||||||
modelValue ? 'bg-primary' : 'bg-border'
|
|
||||||
)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="cn(
|
|
||||||
'inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform',
|
|
||||||
modelValue ? 'translate-x-5' : 'translate-x-1'
|
|
||||||
)"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { VNode } from 'vue';
|
import { VNode } from 'vue';
|
||||||
|
|
||||||
interface Trend {
|
interface Trend {
|
||||||
@@ -6,7 +7,7 @@ interface Trend {
|
|||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
export interface StatProps {
|
||||||
title: string;
|
title: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon?: string | VNode;
|
icon?: string | VNode;
|
||||||
@@ -14,10 +15,12 @@ interface Props {
|
|||||||
color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<StatProps>(), {
|
||||||
color: 'primary'
|
color: 'primary'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// const gradients = {
|
// const gradients = {
|
||||||
// primary: 'from-primary/20 to-primary/5',
|
// primary: 'from-primary/20 to-primary/5',
|
||||||
// success: 'from-success/20 to-success/5',
|
// success: 'from-success/20 to-success/5',
|
||||||
@@ -37,7 +40,7 @@ const iconColors = {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[
|
<div :class="[
|
||||||
'transform translate-y-0 relative overflow-hidden rounded-2xl p-6 bg-surface',
|
'transform translate-y-0 relative overflow-hidden rounded-2xl p-6 bg-header',
|
||||||
// gradients[color],
|
// gradients[color],
|
||||||
'border border-gray-300 transition-all duration-300',
|
'border border-gray-300 transition-all duration-300',
|
||||||
// 'group cursor-pointer'
|
// 'group cursor-pointer'
|
||||||
@@ -46,7 +49,7 @@ const iconColors = {
|
|||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-gray-600 mb-1">{{ title }}</p>
|
<p class="text-sm font-medium text-gray-600 mb-1">{{ $t(title) }}</p>
|
||||||
<p class="text-3xl font-bold text-gray-900">{{ value }}</p>
|
<p class="text-3xl font-bold text-gray-900">{{ value }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ const iconColors = {
|
|||||||
</svg>
|
</svg>
|
||||||
{{ Math.abs(trend.value) }}%
|
{{ Math.abs(trend.value) }}%
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-500">vs last month</span>
|
<span class="text-gray-500">{{ t('overview.stats.trendVsLastMonth') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
fill="#a6acb9" />
|
fill="#a6acb9" />
|
||||||
<path
|
<path
|
||||||
d="M74 42c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32V74c0-18-14-32-32-32H74zM10 74c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H74c-35 0-64-29-64-64V74zm208 256v-80h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
|
d="M74 42c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32V74c0-18-14-32-32-32H74zM10 74c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H74c-35 0-64-29-64-64V74zm208 256v-80h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="v-mid m-a" height="24" viewBox="-10 -226 468 468">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="v-mid m-a" height="24" viewBox="-10 -226 468 468">
|
||||||
<path
|
<path
|
||||||
d="M64-184c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32v-320c0-18-14-32-32-32H64zM0-152c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H64c-35 0-64-29-64-64v-320zm208 256V24h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
|
d="M64-184c-18 0-32 14-32 32v320c0 18 14 32 32 32h320c18 0 32-14 32-32v-320c0-18-14-32-32-32H64zM0-152c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H64c-35 0-64-29-64-64v-320zm208 256V24h-80c-9 0-16-7-16-16s7-16 16-16h80v-80c0-9 7-16 16-16s16 7 16 16v80h80c9 0 16 7 16 16s-7 16-16 16h-80v80c0 9-7 16-16 16s-16-7-16-16z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="#a6acb9"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="currentColor"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="518" viewBox="-10 -244 500 518"><path d="M461-229c12 5 19 16 19 29v416c0 13-7 24-19 29-11 5-25 3-34-5l-47-41c-43-38-98-60-156-63v96c0 18-14 32-32 32h-32c-18 0-32-14-32-32v-96C57 136 0 79 0 8s57-128 128-128h85c61 0 121-23 167-63l47-41c9-8 23-10 34-5zM224 72c70 3 138 29 192 74v-276c-54 45-122 71-192 74V72z" fill="currentColor"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -242 500 516"><path d="M448-194v404l-26-24c-50-47-114-75-182-81V-89c68-6 132-34 182-81l26-24zM240 137c60 6 116 31 160 72l34 32c5 4 12 7 19 7 15 0 27-12 27-27v-425c0-16-12-28-27-28-7 0-14 3-19 8l-34 31c-50 47-116 73-185 73h-87C57-120 0-63 0 8c0 60 41 110 96 124v84c0 27 22 48 48 48h48c27 0 48-21 48-48v-79zm-40-1h8v80c0 9-7 16-16 16h-48c-9 0-16-7-16-16v-80h72zm0-224h8v192h-80c-53 0-96-43-96-96s43-96 96-96h72z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 532"><path d="M10 391c0 19 16 35 36 35h376c20 0 36-16 36-35 0-9-3-16-8-23l-10-12c-30-37-46-84-46-132v-22c0-77-55-142-128-157v-3c0-18-14-32-32-32s-32 14-32 32v3C129 60 74 125 74 202v22c0 48-16 95-46 132l-10 12c-5 7-8 14-8 23z" fill="#a6acb9"/><path d="M172 474c7 28 32 48 62 48s55-20 62-48H172z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 532"><path d="M10 391c0 19 16 35 36 35h376c20 0 36-16 36-35 0-9-3-16-8-23l-10-12c-30-37-46-84-46-132v-22c0-77-55-142-128-157v-3c0-18-14-32-32-32s-32 14-32 32v3C129 60 74 125 74 202v22c0 48-16 95-46 132l-10 12c-5 7-8 14-8 23z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M172 474c7 28 32 48 62 48s55-20 62-48H172z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -258 468 532">
|
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -258 468 532">
|
||||||
<path
|
<path
|
||||||
d="M224-248c-13 0-24 11-24 24v10C119-203 56-133 56-48v15C56 4 46 41 27 74L5 111c-3 6-5 13-5 19 0 21 17 38 38 38h372c21 0 38-17 38-38 0-6-2-13-5-19l-22-37c-19-33-29-70-29-108v-14c0-85-63-155-144-166v-10c0-13-11-24-24-24zm168 368H56l12-22c24-40 36-85 36-131v-15c0-66 54-120 120-120s120 54 120 120v15c0 46 12 91 36 131l12 22zm-236 96c10 28 37 48 68 48s58-20 68-48H156z"
|
d="M224-248c-13 0-24 11-24 24v10C119-203 56-133 56-48v15C56 4 46 41 27 74L5 111c-3 6-5 13-5 19 0 21 17 38 38 38h372c21 0 38-17 38-38 0-6-2-13-5-19l-22-37c-19-33-29-70-29-108v-14c0-85-63-155-144-166v-10c0-13-11-24-24-24zm168 368H56l12-22c24-40 36-85 36-131v-15c0-66 54-120 120-120s120 54 120 120v15c0 46 12 91 36 131l12 22zm-236 96c10 28 37 48 68 48s58-20 68-48H156z"
|
||||||
@@ -8,4 +8,4 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 580 524"><path d="M10 234v112c0 46 38 84 84 84s84-38 84-84V234c0-46-38-84-84-84s-84 38-84 84zM206 94v252c0 46 38 84 84 84s84-38 84-84V94c0-46-38-84-84-84s-84 38-84 84zm196 56v196c0 46 38 84 84 84s84-38 84-84V150c0-46-38-84-84-84s-84 38-84 84z" fill="#a6acb9"/><path d="M10 500c0-8 6-14 14-14h532c8 0 14 6 14 14s-6 14-14 14H24c-8 0-14-6-14-14z" fill="#1e3050"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 580 524"><path d="M10 234v112c0 46 38 84 84 84s84-38 84-84V234c0-46-38-84-84-84s-84 38-84 84zM206 94v252c0 46 38 84 84 84s84-38 84-84V94c0-46-38-84-84-84s-84 38-84 84zm196 56v196c0 46 38 84 84 84s84-38 84-84V150c0-46-38-84-84-84s-84 38-84 84z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M10 500c0-8 6-14 14-14h532c8 0 14 6 14 14s-6 14-14 14H24c-8 0-14-6-14-14z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 532 468"><path d="M272-184c9 0 16 7 16 16v352c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-352c0-9 7-16 16-16h32zm-32-32c-26 0-48 22-48 48v352c0 27 22 48 48 48h32c27 0 48-21 48-48v-352c0-26-21-48-48-48h-32zM80 8c9 0 16 7 16 16v160c0 9-7 16-16 16H48c-9 0-16-7-16-16V24c0-9 7-16 16-16h32zM48-24C22-24 0-2 0 24v160c0 27 22 48 48 48h32c27 0 48-21 48-48V24c0-26-21-48-48-48H48zm384-96h32c9 0 16 7 16 16v288c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-288c0-9 7-16 16-16zm-48 16v288c0 27 22 48 48 48h32c27 0 48-21 48-48v-288c0-26-21-48-48-48h-32c-26 0-48 22-48 48z" fill="#1e3050"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 532 468"><path d="M272-184c9 0 16 7 16 16v352c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-352c0-9 7-16 16-16h32zm-32-32c-26 0-48 22-48 48v352c0 27 22 48 48 48h32c27 0 48-21 48-48v-352c0-26-21-48-48-48h-32zM80 8c9 0 16 7 16 16v160c0 9-7 16-16 16H48c-9 0-16-7-16-16V24c0-9 7-16 16-16h32zM48-24C22-24 0-2 0 24v160c0 27 22 48 48 48h32c27 0 48-21 48-48V24c0-26-21-48-48-48H48zm384-96h32c9 0 16 7 16 16v288c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-288c0-9 7-16 16-16zm-48 16v288c0 27 22 48 48 48h32c27 0 48-21 48-48v-288c0-26-21-48-48-48h-32c-26 0-48 22-48 48z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 532"><path d="M26 427c0-121 70-194 157-273h134c87 79 157 152 157 273 0 44-35 79-79 79H105c-44 0-79-35-79-79zM138 42c0-9 7-16 16-16h192c9 0 16 7 16 16 0 3 0 5-2 8l-46 88H187l-47-88c-1-3-2-5-2-8zm56 267c0 21 15 38 36 42l38 6c13 2 22 13 22 26 0 15-12 27-27 27h-53c-4 0-8 4-8 8s4 8 8 8h32v16c0 4 4 8 8 8s8-4 8-8v-16h5c24 0 43-19 43-43 0-20-15-38-36-42l-38-6c-12-2-22-13-22-26 0-15 12-27 27-27h45c4 0 8-3 8-8 0-4-4-8-8-8h-24v-16c0-4-4-8-8-8s-8 4-8 8v16h-5c-24 0-43 19-43 43z" fill="#a6acb9"/><path d="M346 26c9 0 16 7 16 16 0 3-1 5-2 8l-46 88H187l-47-88c-1-3-2-5-2-8 0-9 7-16 16-16h192zM126 57l45 86C85 222 10 299 10 427c0 52 43 95 95 95h290c52 0 95-43 95-95 0-128-75-205-161-284l45-86c3-5 4-10 4-15 0-18-14-32-32-32H154c-18 0-32 14-32 32 0 5 1 10 4 15zM26 427c0-121 70-194 157-273h134c87 79 157 152 157 273 0 44-35 79-79 79H105c-44 0-79-35-79-79zm224-185c-4 0-8 4-8 8v16h-5c-24 0-43 19-43 43 0 20 15 38 36 42l38 6c13 2 22 13 22 26 0 15-12 27-27 27h-53c-4 0-8 4-8 8s4 8 8 8h32v16c0 4 4 8 8 8s8-4 8-8v-16h5c24 0 43-19 43-43 0-20-15-38-36-42l-38-6c-12-2-22-13-22-26 0-15 12-27 27-27h45c4 0 8-3 8-8 0-4-4-8-8-8h-24v-16c0-4-4-8-8-8z" fill="currentColor"/></svg>
|
||||||
<circle cx="12" cy="12" r="9"/>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="532" viewBox="6 -258 500 532"><path d="m379-191-46 81c84 77 163 154 163 279 0 52-43 95-95 95H111c-52 0-95-43-95-95C16 44 96-33 179-110l-46-81c-3-6-5-12-5-19 0-21 17-38 38-38h180c21 0 38 17 38 38 0 7-2 13-5 19zM227-88l-1 1C134-4 64 61 64 169c0 26 21 47 47 47h290c26 0 47-21 47-47C448 61 378-4 286-87l-1-1h-58zm-7-48h72l37-64H183l37 64zm40 96c11 0 20 9 20 20v4h8c11 0 20 9 20 20s-9 20-20 20h-47c-7 0-13 6-13 13 0 6 4 11 10 12l42 7c25 4 44 26 44 52s-19 47-44 51v5c0 11-9 20-20 20s-20-9-20-20v-4h-24c-11 0-20-9-20-20s9-20 20-20h56c6 0 12-5 12-12 0-6-4-12-10-13l-42-7c-25-4-44-26-44-51 0-29 23-53 52-53v-4c0-11 9-20 20-20z" fill="currentColor"/></svg>
|
||||||
<path d="M12 16V8"/>
|
|
||||||
<path d="M9.5 10a2.5 2.5 0 0 1 5 0v4a2.5 2.5 0 0 1-5 0"/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="#a6acb9"/><path d="M10 106h512v64H10zm0 0z" fill="#1e3050"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M10 106h512v64H10zm0 0z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -194 532 404"><path d="M448-136c9 0 16 7 16 16v32H48v-32c0-9 7-16 16-16h384zm16 112v160c0 9-7 16-16 16H64c-9 0-16-7-16-16V-24h416zM64-184c-35 0-64 29-64 64v256c0 35 29 64 64 64h384c35 0 64-29 64-64v-256c0-35-29-64-64-64H64zM80 96c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24h-48c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="#1e3050"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -194 532 404"><path d="M448-136c9 0 16 7 16 16v32H48v-32c0-9 7-16 16-16h384zm16 112v160c0 9-7 16-16 16H64c-9 0-16-7-16-16V-24h416zM64-184c-35 0-64 29-64 64v256c0 35 29 64 64 64h384c35 0 64-29 64-64v-256c0-35-29-64-64-64H64zM80 96c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24h-48c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
fill="#a6acb9" />
|
fill="#a6acb9" />
|
||||||
<path
|
<path
|
||||||
d="M208 26c3 0 7 0 10 1v103c0 31 25 56 56 56h103c1 3 1 7 1 11v261c0 27-21 48-48 48H74c-26 0-48-21-48-48V74c0-26 22-48 48-48h134zm156 137c2 2 4 4 6 7h-96c-22 0-40-18-40-40V34c3 2 5 4 7 6l123 123zM74 10c-35 0-64 29-64 64v384c0 35 29 64 64 64h256c35 0 64-29 64-64V197c0-17-7-34-19-46L253 29c-12-12-28-19-45-19H74zm144 272c9 0 16 7 16 16v96c0 9-7 16-16 16h-96c-9 0-16-7-16-16v-96c0-9 7-16 16-16h96zm-96-16c-18 0-32 14-32 32v96c0 18 14 32 32 32h96c18 0 32-14 32-32v-18l40 25c10 7 24-1 24-13v-84c0-12-14-20-24-13l-40 25v-18c0-18-14-32-32-32h-96zm176 38v84l-48-30v-24l48-30z"
|
d="M208 26c3 0 7 0 10 1v103c0 31 25 56 56 56h103c1 3 1 7 1 11v261c0 27-21 48-48 48H74c-26 0-48-21-48-48V74c0-26 22-48 48-48h134zm156 137c2 2 4 4 6 7h-96c-22 0-40-18-40-40V34c3 2 5 4 7 6l123 123zM74 10c-35 0-64 29-64 64v384c0 35 29 64 64 64h256c35 0 64-29 64-64V197c0-17-7-34-19-46L253 29c-12-12-28-19-45-19H74zm144 272c9 0 16 7 16 16v96c0 9-7 16-16 16h-96c-9 0-16-7-16-16v-96c0-9 7-16 16-16h96zm-96-16c-18 0-32 14-32 32v96c0 18 14 32 32 32h96c18 0 32-14 32-32v-18l40 25c10 7 24-1 24-13v-84c0-12-14-20-24-13l-40 25v-18c0-18-14-32-32-32h-96zm176 38v84l-48-30v-24l48-30z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<!-- Remote link icon -->
|
<!-- Remote link icon -->
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 564">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 564">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
fill="#a6acb9" />
|
fill="#a6acb9" />
|
||||||
<path
|
<path
|
||||||
d="M570 274H458c-2 118-55 195-99 230 116-14 207-111 211-230zM269 498c10 3 21 5 32 6-9-7-18-16-27-26l6-18c18 22 36 37 50 45 40-22 109-99 112-231H335l4-16h103c-3-132-72-209-112-231-39 22-107 96-112 224l-16 5c3-117 56-193 99-228C185 42 94 139 90 258h104l-55 16H90c0 5 1 10 1 14l-16 5c0-9-1-18-1-27C74 125 189 10 330 10s256 115 256 256-115 256-256 256c-23 0-45-3-66-9l5-15zm301-240c-4-119-95-216-211-230 44 35 97 112 99 230h112zM150 414l2 5 46 92 60-205-204 60 91 46 5 2zM31 373l-21-11 23-7 231-68 18-5-5 18-68 232-7 22-60-120-94 94-6 5-11-11 5-6 95-94-100-49z"
|
d="M570 274H458c-2 118-55 195-99 230 116-14 207-111 211-230zM269 498c10 3 21 5 32 6-9-7-18-16-27-26l6-18c18 22 36 37 50 45 40-22 109-99 112-231H335l4-16h103c-3-132-72-209-112-231-39 22-107 96-112 224l-16 5c3-117 56-193 99-228C185 42 94 139 90 258h104l-55 16H90c0 5 1 10 1 14l-16 5c0-9-1-18-1-27C74 125 189 10 330 10s256 115 256 256-115 256-256 256c-23 0-45-3-66-9l5-15zm301-240c-4-119-95-216-211-230 44 35 97 112 99 230h112zM150 414l2 5 46 92 60-205-204 60 91 46 5 2zM31 373l-21-11 23-7 231-68 18-5-5 18-68 232-7 22-60-120-94 94-6 5-11-11 5-6 95-94-100-49z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 503"><path d="M10 397v32c0 35 29 64 64 64h320c35 0 64-29 64-64v-32c0-35-29-64-64-64H266v32c0 18-14 32-32 32s-32-14-32-32v-32H74c-35 0-64 29-64 64zm392 16c0 13-11 24-24 24s-24-11-24-24 11-24 24-24 24 11 24 24z" fill="#a6acb9"/><path d="M234 397c18 0 32-14 32-32V122l41 42c13 12 33 12 46 0 12-13 12-33 0-46l-96-96c-13-12-33-12-46 0l-96 96c-12 13-12 33 0 46 13 12 33 12 46 0l41-42v243c0 18 14 32 32 32z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 503"><path d="M10 397v32c0 35 29 64 64 64h320c35 0 64-29 64-64v-32c0-35-29-64-64-64H266v32c0 18-14 32-32 32s-32-14-32-32v-32H74c-35 0-64 29-64 64zm392 16c0 13-11 24-24 24s-24-11-24-24 11-24 24-24 24 11 24 24z" fill="#a6acb9"/><path d="M234 397c18 0 32-14 32-32V122l41 42c13 12 33 12 46 0 12-13 12-33 0-46l-96-96c-13-12-33-12-46 0l-96 96c-12 13-12 33 0 46 13 12 33 12 46 0l41-42v243c0 18 14 32 32 32z" fill="currentColor"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -260 468 502"><path d="M248 80c0 13-11 24-24 24s-24-11-24-24v-246l-63 63c-9 9-25 9-34 0s-9-25 0-34l104-104c9-9 25-9 34 0l104 104c9 9 9 25 0 34s-25 9-34 0l-63-63V80zm-96-8H64c-9 0-16 7-16 16v80c0 9 7 16 16 16h320c9 0 16-7 16-16V88c0-9-7-16-16-16h-88V24h88c35 0 64 29 64 64v80c0 35-29 64-64 64H64c-35 0-64-29-64-64V88c0-35 29-64 64-64h88v48zm168 56c0-13 11-24 24-24s24 11 24 24-11 24-24 24-24-11-24-24z" fill="#1e3050"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -260 468 502"><path d="M248 80c0 13-11 24-24 24s-24-11-24-24v-246l-63 63c-9 9-25 9-34 0s-9-25 0-34l104-104c9-9 25-9 34 0l104 104c9 9 9 25 0 34s-25 9-34 0l-63-63V80zm-96-8H64c-9 0-16 7-16 16v80c0 9 7 16 16 16h320c9 0 16-7 16-16V88c0-9-7-16-16-16h-88V24h88c35 0 64 29 64 64v80c0 35-29 64-64 64H64c-35 0-64-29-64-64V88c0-35 29-64 64-64h88v48zm168 56c0-13 11-24 24-24s24 11 24 24-11 24-24 24-24-11-24-24z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 539 535">
|
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 539 535">
|
||||||
<path d="M61 281c2-1 4-3 6-5L269 89l202 187c2 2 4 4 6 5v180c0 35-29 64-64 64H125c-35 0-64-29-64-64V281z"
|
<path d="M61 281c2-1 4-3 6-5L269 89l202 187c2 2 4 4 6 5v180c0 35-29 64-64 64H125c-35 0-64-29-64-64V281z"
|
||||||
fill="#a6acb9" />
|
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||||
<path
|
<path
|
||||||
d="M247 22c13-12 32-12 44 0l224 208c13 12 13 32 1 45s-32 14-45 2L269 89 67 276c-13 12-33 12-45-1s-12-33 1-45L247 22z"
|
d="M247 22c13-12 32-12 44 0l224 208c13 12 13 32 1 45s-32 14-45 2L269 89 67 276c-13 12-33 12-45-1s-12-33 1-45L247 22z"
|
||||||
fill="#1e3050" />
|
fill="var(--colors-primary-DEFAULT)" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-11 -259 535 533">
|
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-11 -259 535 533">
|
||||||
<path
|
<path
|
||||||
d="M272-242c-9-8-23-8-32 0L8-34C-2-25-3-10 6 0s24 11 34 2l8-7v205c0 35 29 64 64 64h288c35 0 64-29 64-64V-5l8 7c10 9 25 8 34-2s8-25-2-34L272-242zM416-48v248c0 9-7 16-16 16H112c-9 0-16-7-16-16V-48l160-144L416-48z"
|
d="M272-242c-9-8-23-8-32 0L8-34C-2-25-3-10 6 0s24 11 34 2l8-7v205c0 35 29 64 64 64h288c35 0 64-29 64-64V-5l8 7c10 9 25 8 34-2s8-25-2-34L272-242zM416-48v248c0 9-7 16-16 16H112c-9 0-16-7-16-16V-48l160-144L416-48z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
fill="#a6acb9" />
|
fill="#a6acb9" />
|
||||||
<path
|
<path
|
||||||
d="M394 42c18 0 32 14 32 32v64H42V74c0-18 14-32 32-32h320zM42 394V170h96v256H74c-18 0-32-14-32-32zm128 32V170h256v224c0 18-14 32-32 32H170zM74 10c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64V74c0-35-29-64-64-64H74z"
|
d="M394 42c18 0 32 14 32 32v64H42V74c0-18 14-32 32-32h320zM42 394V170h96v256H74c-18 0-32-14-32-32zm128 32V170h256v224c0 18-14 32-32 32H170zM74 10c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64V74c0-35-29-64-64-64H74z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else class="v-mid m-a" height="24" width="24" viewBox="-10 -226 468 468">
|
<svg xmlns="http://www.w3.org/2000/svg" v-else class="v-mid m-a" height="24" width="24" viewBox="-10 -226 468 468">
|
||||||
<path
|
<path
|
||||||
d="M384-184c18 0 32 14 32 32v64H32v-64c0-18 14-32 32-32h320zM32 168V-56h96v256H64c-18 0-32-14-32-32zm128 32V-56h256v224c0 18-14 32-32 32H160zM64-216c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64v-320c0-35-29-64-64-64H64z"
|
d="M384-184c18 0 32 14 32 32v64H32v-64c0-18 14-32 32-32h320zM32 168V-56h96v256H64c-18 0-32-14-32-32zm128 32V-56h256v224c0 18-14 32-32 32H160zM64-216c-35 0-64 29-64 64v320c0 35 29 64 64 64h320c35 0 64-29 64-64v-320c0-35-29-64-64-64H64z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 404"><path d="M74 170v64c0 53 43 96 96 96h96v64h64v-64h96c53 0 96-43 96-96v-64c0-53-43-96-96-96h-96V10h-64v64h-96c-53 0-96 43-96 96zm96 0h256v64H170v-64z" fill="#a6acb9"/><path d="M170 10C82 10 10 82 10 170v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96V10h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM202 170h-32v64h256v-64H202z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 404"><path d="M74 170v64c0 53 43 96 96 96h96v64h64v-64h96c53 0 96-43 96-96v-64c0-53-43-96-96-96h-96V10h-64v64h-96c-53 0-96 43-96 96zm96 0h256v64H170v-64z" fill="#a6acb9"/><path d="M170 10C82 10 10 82 10 170v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96V10h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM202 170h-32v64h256v-64H202z" fill="currentColor"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -194 596 404"><path d="M160-184C72-184 0-112 0-24v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96v-64h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM192-24h-32v64h256v-64H192z" fill="currentColor"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -194 596 404"><path d="M160-184C72-184 0-112 0-24v64c0 88 72 160 160 160h96v-64h-96c-53 0-96-43-96-96v-64c0-53 43-96 96-96h96v-64h-96zm256 384c88 0 160-72 160-160v-64c0-88-72-160-160-160h-96v64h96c53 0 96 43 96 96v64c0 53-43 96-96 96h-96v64h96zM192-24h-32v64h256v-64H192z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 580"><path d="M18 190c-8 14-6 32 5 43l37 36v42l-37 36c-11 12-13 29-5 43l46 80c8 14 24 21 40 17l50-14c11 8 23 15 36 21l13 50c4 15 18 26 34 26h93c16 0 30-11 34-26l13-50c13-6 25-13 36-21l50 14c15 4 32-3 40-17l46-80c8-14 6-31-6-43l-37-36c1-7 1-14 1-21s0-14-1-21l37-36c12-11 14-29 6-43l-46-80c-8-14-24-21-40-17l-50 14c-11-8-23-15-36-21l-13-50c-4-15-18-26-34-26h-93c-16 0-30 11-34 26l-13 50c-13 6-25 13-36 21l-50-13c-16-5-32 2-40 16l-46 80zm377 100c1 41-20 79-55 99-35 21-79 21-114 0-35-20-56-58-54-99-2-41 19-79 54-99 35-21 79-21 114 0 35 20 56 58 55 99zm-195 0c-2 31 14 59 40 75 27 15 59 15 86 0 26-16 42-44 41-75 1-31-15-59-41-75-27-15-59-15-86 0-26 16-42 44-40 75z" fill="#a6acb9"/><path d="M283 206c46 0 84 37 84 84 0 46-37 84-83 84-47 0-85-37-85-84 0-46 37-84 84-84zm1 196c61 0 111-51 111-112 0-62-51-112-112-112-62 0-112 51-112 112 0 62 51 112 113 112z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 580"><path d="M18 190c-8 14-6 32 5 43l37 36v42l-37 36c-11 12-13 29-5 43l46 80c8 14 24 21 40 17l50-14c11 8 23 15 36 21l13 50c4 15 18 26 34 26h93c16 0 30-11 34-26l13-50c13-6 25-13 36-21l50 14c15 4 32-3 40-17l46-80c8-14 6-31-6-43l-37-36c1-7 1-14 1-21s0-14-1-21l37-36c12-11 14-29 6-43l-46-80c-8-14-24-21-40-17l-50 14c-11-8-23-15-36-21l-13-50c-4-15-18-26-34-26h-93c-16 0-30 11-34 26l-13 50c-13 6-25 13-36 21l-50-13c-16-5-32 2-40 16l-46 80zm377 100c1 41-20 79-55 99-35 21-79 21-114 0-35-20-56-58-54-99-2-41 19-79 54-99 35-21 79-21 114 0 35 20 56 58 55 99zm-195 0c-2 31 14 59 40 75 27 15 59 15 86 0 26-16 42-44 41-75 1-31-15-59-41-75-27-15-59-15-86 0-26 16-42 44-40 75z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M283 206c46 0 84 37 84 84 0 46-37 84-83 84-47 0-85-37-85-84 0-46 37-84 84-84zm1 196c61 0 111-51 111-112 0-62-51-112-112-112-62 0-112 51-112 112 0 62 51 112 113 112z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" class="min-w-[28px]" viewBox="0 0 596 468">
|
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" class="min-w-[28px]" viewBox="0 0 596 468">
|
||||||
<path
|
<path
|
||||||
d="M10 314c0-63 41-117 98-136-1-8-2-16-2-24 0-79 65-144 144-144 55 0 104 31 128 77 14-8 30-13 48-13 53 0 96 43 96 96 0 16-4 31-10 44 44 20 74 64 74 116 0 71-57 128-128 128H154c-79 0-144-64-144-144zm199-73c-9 9-9 25 0 34s25 9 34 0l31-31v102c0 13 11 24 24 24s24-11 24-24V244l31 31c9 9 25 9 34 0s9-25 0-34l-72-72c-10-9-25-9-34 0l-72 72z"
|
d="M10 314c0-63 41-117 98-136-1-8-2-16-2-24 0-79 65-144 144-144 55 0 104 31 128 77 14-8 30-13 48-13 53 0 96 43 96 96 0 16-4 31-10 44 44 20 74 64 74 116 0 71-57 128-128 128H154c-79 0-144-64-144-144zm199-73c-9 9-9 25 0 34s25 9 34 0l31-31v102c0 13 11 24 24 24s24-11 24-24V244l31 31c9 9 25 9 34 0s9-25 0-34l-72-72c-10-9-25-9-34 0l-72 72z"
|
||||||
fill="#a6acb9" />
|
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||||
<path
|
<path
|
||||||
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
|
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
|
||||||
fill="#1e3050" />
|
fill="var(--colors-primary-DEFAULT)" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else class="min-w-[28px]" viewBox="-10 -226 596 468">
|
<svg xmlns="http://www.w3.org/2000/svg" v-else class="min-w-[28px]" viewBox="-10 -226 596 468">
|
||||||
<path
|
<path
|
||||||
d="M240-216c-88 0-160 72-160 160 0 5 0 10 1 15C33-18 0 31 0 88c0 80 65 144 144 144h304c71 0 128-57 128-128 0-50-28-93-70-114 4-12 6-25 6-38 0-66-54-120-120-120-11 0-23 2-33 5-30-33-72-53-119-53zM128-56c0-62 50-112 112-112 38 0 71 19 91 47 7 10 20 13 30 8 9-4 20-7 31-7 40 0 72 32 72 72 0 14-4 27-11 38-4 7-5 15-2 22s9 13 16 14c35 9 61 41 61 78 0 44-36 80-80 80H144c-53 0-96-43-96-96 0-43 28-79 67-91 11-4 18-16 16-29-2-7-3-16-3-24zm177 7c-9-9-25-9-34 0l-64 64c-9 9-9 25 0 34 10 9 25 9 34 0l23-23v86c0 13 11 24 24 24s24-11 24-24V26l23 23c9 9 25 9 34 0s9-25 0-34l-64-64z"
|
d="M240-216c-88 0-160 72-160 160 0 5 0 10 1 15C33-18 0 31 0 88c0 80 65 144 144 144h304c71 0 128-57 128-128 0-50-28-93-70-114 4-12 6-25 6-38 0-66-54-120-120-120-11 0-23 2-33 5-30-33-72-53-119-53zM128-56c0-62 50-112 112-112 38 0 71 19 91 47 7 10 20 13 30 8 9-4 20-7 31-7 40 0 72 32 72 72 0 14-4 27-11 38-4 7-5 15-2 22s9 13 16 14c35 9 61 41 61 78 0 44-36 80-80 80H144c-53 0-96-43-96-96 0-43 28-79 67-91 11-4 18-16 16-29-2-7-3-16-3-24zm177 7c-9-9-25-9-34 0l-64 64c-9 9-9 25 0 34 10 9 25 9 34 0l23-23v86c0 13 11 24 24 24s24-11 24-24V26l23 23c9 9 25 9 34 0s9-25 0-34l-64-64z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
10
src/components/icons/UserIcon copy.vue
Normal file
10
src/components/icons/UserIcon copy.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
filled?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="currentColor"/></svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="currentColor"/></svg>
|
||||||
|
</template>
|
||||||
@@ -5,6 +5,6 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="#a6acb9"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="currentColor"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="#1e3050"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="518" height="532" viewBox="-3 -258 518 532"><path d="M368 120h-33l-22-64H199l-21 64h-34l32-96h160l32 96zM256-8c-35 0-64-29-64-64s29-64 64-64c36 0 64 29 64 64S292-8 256-8zm0-96c-17 0-32 14-32 32s15 32 32 32c18 0 32-14 32-32s-14-32-32-32zm0 368-12-5C92 193 7 26 17-135l1-20 238-93 239 93 1 20c9 161-76 328-227 394l-13 5zM49-133c-7 147 67 302 207 362 140-60 215-215 208-362l-208-81-207 81z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404">
|
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404">
|
||||||
<path d="M10 74v256c0 35 29 64 64 64h256c35 0 64-29 64-64V74c0-35-29-64-64-64H74c-35 0-64 29-64 64z"
|
<path d="M10 74v256c0 35 29 64 64 64h256c35 0 64-29 64-64V74c0-35-29-64-64-64H74c-35 0-64 29-64 64z"
|
||||||
fill="#a6acb9" />
|
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||||
<path d="M394 135v134l90 72c4 3 9 5 14 5 13 0 24-11 24-24V82c0-13-11-24-24-24-5 0-10 2-14 5l-90 72z"
|
<path d="M394 135v134l90 72c4 3 9 5 14 5 13 0 24-11 24-24V82c0-13-11-24-24-24-5 0-10 2-14 5l-90 72z"
|
||||||
fill="#1e3050" />
|
fill="var(--colors-primary-DEFAULT)" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -194 564 404">
|
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -194 564 404">
|
||||||
<path
|
<path
|
||||||
d="M96-136c-9 0-16 7-16 16v256c0 9 7 16 16 16h256c9 0 16-7 16-16v-256c0-9-7-16-16-16H96zm-64 16c0-35 29-64 64-64h256c35 0 64 29 64 64v256c0 35-29 64-64 64H96c-35 0-64-29-64-64v-256zm506-11c4-3 9-5 14-5 13 0 24 11 24 24v240c0 13-11 24-24 24-5 0-10-2-14-5l-74-55V32l64 48V-64l-64 48v-60l74-55z"
|
d="M96-136c-9 0-16 7-16 16v256c0 9 7 16 16 16h256c9 0 16-7 16-16v-256c0-9-7-16-16-16H96zm-64 16c0-35 29-64 64-64h256c35 0 64 29 64 64v256c0 35-29 64-64 64H96c-35 0-64-29-64-64v-256zm506-11c4-3 9-5 14-5 13 0 24 11 24 24v240c0 13-11 24-24 24-5 0-10-2-14-5l-74-55V32l64 48V-64l-64 48v-60l74-55z"
|
||||||
fill="#1e3050" />
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{ filled?: boolean }>();
|
defineProps<{ filled?: boolean }>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M42 170h241c-40 35-65 87-65 144 0 17 2 33 6 48H74c-18 0-32-14-32-32V170z" fill="#a6acb9"/><path d="M458 42H345l-96 96h84c-18 8-35 19-50 32H42v160c0 18 14 32 32 32h150c3 11 7 22 11 32H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3V74c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM43 138l96-96H74c-18 0-32 14-32 32v64h1zm46 0h114l96-96H185l-96 96zm321 288c62 0 112-50 112-112s-50-112-112-112-112 50-112 112 50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144 64-144 144-144zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V258c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="#1e3050"/></svg>
|
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M42 170h241c-40 35-65 87-65 144 0 17 2 33 6 48H74c-18 0-32-14-32-32V170z" fill="#a6acb9"/><path d="M458 42H345l-96 96h84c-18 8-35 19-50 32H42v160c0 18 14 32 32 32h150c3 11 7 22 11 32H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3V74c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM43 138l96-96H74c-18 0-32 14-32 32v64h1zm46 0h114l96-96H185l-96 96zm321 288c62 0 112-50 112-112s-50-112-112-112-112 50-112 112 50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144 64-144 144-144zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V258c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="564" height="468" viewBox="22 -194 564 468"><path d="M480-152H367l-96 96h84c-18 8-35 19-50 32H64v160c0 18 14 32 32 32h150c3 11 7 22 11 32H96c-35 0-64-29-64-64v-256c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3v-64c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM65-56l96-96H96c-18 0-32 14-32 32v64h1zm46 0h114l96-96H207l-96 96zm321 288c62 0 112-50 112-112S494 8 432 8 320 58 320 120s50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144S352-24 432-24zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V64c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="#1e3050"/></svg>
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="564" height="468" viewBox="22 -194 564 468"><path d="M480-152H367l-96 96h84c-18 8-35 19-50 32H64v160c0 18 14 32 32 32h150c3 11 7 22 11 32H96c-35 0-64-29-64-64v-256c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3v-64c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM65-56l96-96H96c-18 0-32 14-32 32v64h1zm46 0h114l96-96H207l-96 96zm321 288c62 0 112-50 112-112S494 8 432 8 320 58 320 120s50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144S352-24 432-24zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V64c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
61
src/components/ui/AppButton.vue
Normal file
61
src/components/ui/AppButton.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
type UiButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||||
|
type UiButtonSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: UiButtonVariant;
|
||||||
|
size?: UiButtonSize;
|
||||||
|
block?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
block: false,
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
type: 'button',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabled = computed(() => props.disabled || props.loading);
|
||||||
|
|
||||||
|
const classes = computed(() => {
|
||||||
|
const variants: Record<UiButtonVariant, string> = {
|
||||||
|
primary: 'border-transparent bg-primary text-white hover:bg-primaryHover focus-visible:ring-primary/25',
|
||||||
|
secondary: 'border-border bg-white text-text hover:bg-header focus-visible:ring-#0969da/20',
|
||||||
|
ghost: 'border-transparent bg-transparent text-text hover:bg-header focus-visible:ring-#0969da/20 shadow-none',
|
||||||
|
danger: 'border-transparent bg-danger text-white hover:opacity-92 focus-visible:ring-danger/20',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes: Record<UiButtonSize, string> = {
|
||||||
|
sm: 'min-h-[28px] px-3 text-[12px] leading-[20px]',
|
||||||
|
md: 'min-h-[32px] px-3 text-[14px] leading-[20px]',
|
||||||
|
lg: 'min-h-[36px] px-4 text-[14px] leading-[20px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap shadow-primer outline-none transition-[transform,box-shadow,background-color,border-color,color,opacity] duration-150 ease-out active:translate-y-[0.5px] hover:shadow-[0_2px_0_rgba(27,31,36,0.06)] disabled:cursor-not-allowed disabled:opacity-60 focus-visible:ring-4',
|
||||||
|
variants[props.variant],
|
||||||
|
sizes[props.size],
|
||||||
|
props.block ? 'w-full' : '',
|
||||||
|
].join(' ');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button :type="type" :disabled="isDisabled" :class="classes" :aria-busy="loading || undefined">
|
||||||
|
<span
|
||||||
|
v-if="loading"
|
||||||
|
class="h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<slot v-else name="icon" />
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppButton from '@/components/app/AppButton.vue';
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
import AppDialog from '@/components/app/AppDialog.vue';
|
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||||
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
||||||
import { useAppConfirm } from '@/composables/useAppConfirm';
|
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import XIcon from '@/components/icons/XIcon.vue';
|
import XIcon from '@/components/icons/XIcon.vue';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
// Ensure client-side only rendering to avoid hydration mismatch
|
// Ensure client-side only rendering to avoid hydration mismatch
|
||||||
@@ -25,6 +26,8 @@ const emit = defineEmits<{
|
|||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
emit('close');
|
emit('close');
|
||||||
@@ -72,7 +75,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- Panel -->
|
<!-- Panel -->
|
||||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||||
<div :class="cn('w-full bg-surface border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
|
<div :class="cn('w-full bg-header border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
|
||||||
<!-- Header slot -->
|
<!-- Header slot -->
|
||||||
<div v-if="$slots.header" class="px-5 py-4 border-b border-border">
|
<div v-if="$slots.header" class="px-5 py-4 border-b border-border">
|
||||||
<slot name="header" :close="close" />
|
<slot name="header" :close="close" />
|
||||||
@@ -87,7 +90,7 @@ onBeforeUnmount(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
class="p-1 rounded-md text-foreground/60 hover:text-foreground hover:bg-muted/50 transition-all"
|
class="p-1 rounded-md text-foreground/60 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||||
@click="close"
|
@click="close"
|
||||||
aria-label="Close"
|
:aria-label="t('common.close')"
|
||||||
>
|
>
|
||||||
<XIcon class="w-4 h-4" />
|
<XIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +61,7 @@ const onKeyup = (e: KeyboardEvent) => {
|
|||||||
if (e.key === 'Enter') emit('enter');
|
if (e.key === 'Enter') emit('enter');
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-surface text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 disabled:opacity-60 disabled:cursor-not-allowed';
|
const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-header text-foreground placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50 disabled:opacity-60 disabled:cursor-not-allowed';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
55
src/components/ui/AppSwitch.vue
Normal file
55
src/components/ui/AppSwitch.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface SwitchProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
class?: string; // Đổi từ className sang class cho chuẩn Vue
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SwitchProps>(), {
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vue 3.4+ - Quản lý v-model cực gọn
|
||||||
|
const modelValue = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'change', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
modelValue.value = !modelValue.value;
|
||||||
|
emit('change', modelValue.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="modelValue"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="toggle"
|
||||||
|
:class="cn(
|
||||||
|
// Layout & Size
|
||||||
|
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors duration-200',
|
||||||
|
// Focus states (UnoCSS style)
|
||||||
|
'outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
||||||
|
// Status states
|
||||||
|
disabled ? 'op-50 cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
modelValue ? 'bg-primary' : 'bg-gray-200 dark:bg-dark-300',
|
||||||
|
props.class
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="cn(
|
||||||
|
// Toggle thumb
|
||||||
|
'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200',
|
||||||
|
modelValue ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -91,7 +91,7 @@ onBeforeUnmount(() => {
|
|||||||
type="button"
|
type="button"
|
||||||
class="p-1 rounded-md text-foreground/50 hover:text-foreground hover:bg-muted/50 transition-all"
|
class="p-1 rounded-md text-foreground/50 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||||
@click="dismiss(t.id)"
|
@click="dismiss(t.id)"
|
||||||
aria-label="Dismiss"
|
:aria-label="$t('toast.dismissAria')"
|
||||||
>
|
>
|
||||||
<XIcon class="w-4 h-4" />
|
<XIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
75
src/components/ui/AsyncSelect.vue
Normal file
75
src/components/ui/AsyncSelect.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loadOptions: () => Promise<SelectOption[]>;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
placeholder: 'Please select...',
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = defineModel<string | number>();
|
||||||
|
|
||||||
|
const options = ref<SelectOption[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
options.value = await props.loadOptions();
|
||||||
|
} catch {
|
||||||
|
error.value = 'Failed to load options';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
|
watch(() => props.loadOptions, fetchData);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="relative w-full">
|
||||||
|
<select
|
||||||
|
v-model="modelValue"
|
||||||
|
:disabled="loading || disabled"
|
||||||
|
class="w-full appearance-none rounded-md border border-border bg-header px-3 py-2 pr-10 text-sm text-foreground outline-none transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<option value="" disabled>{{ placeholder }}</option>
|
||||||
|
<option
|
||||||
|
v-for="opt in options"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-foreground/40">
|
||||||
|
<div v-if="loading" class="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
|
||||||
|
<div v-else class="i-carbon-chevron-down text-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="error"
|
||||||
|
type="button"
|
||||||
|
@click="fetchData"
|
||||||
|
class="text-xs font-medium text-danger transition hover:opacity-80"
|
||||||
|
>
|
||||||
|
{{ error }} · Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
153
src/components/ui/BaseTable.vue
Normal file
153
src/components/ui/BaseTable.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script setup lang="ts" generic="TData extends Record<string, any>">
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
FlexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useVueTable,
|
||||||
|
type ColumnMeta,
|
||||||
|
type ColumnDef,
|
||||||
|
type Row,
|
||||||
|
type SortingState,
|
||||||
|
type Updater,
|
||||||
|
} from '@tanstack/vue-table';
|
||||||
|
|
||||||
|
type TableColumnMeta = ColumnMeta<TData, any> & {
|
||||||
|
headerClass?: string;
|
||||||
|
cellClass?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
data: TData[];
|
||||||
|
columns: ColumnDef<TData, any>[];
|
||||||
|
loading?: boolean;
|
||||||
|
emptyText?: string;
|
||||||
|
tableClass?: string;
|
||||||
|
wrapperClass?: string;
|
||||||
|
headerRowClass?: string;
|
||||||
|
bodyRowClass?: string | ((row: Row<TData>) => string | undefined);
|
||||||
|
getRowId?: (originalRow: TData, index: number) => string;
|
||||||
|
}>(), {
|
||||||
|
loading: false,
|
||||||
|
emptyText: 'No data available.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorting = ref<SortingState>([]);
|
||||||
|
|
||||||
|
function updateSorting(updaterOrValue: Updater<SortingState>) {
|
||||||
|
sorting.value = typeof updaterOrValue === 'function'
|
||||||
|
? updaterOrValue(sorting.value)
|
||||||
|
: updaterOrValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = useVueTable<TData>({
|
||||||
|
get data() {
|
||||||
|
return props.data;
|
||||||
|
},
|
||||||
|
get columns() {
|
||||||
|
return props.columns;
|
||||||
|
},
|
||||||
|
getRowId: props.getRowId,
|
||||||
|
state: {
|
||||||
|
get sorting() {
|
||||||
|
return sorting.value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSortingChange: updateSorting,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function resolveBodyRowClass(row: Row<TData>) {
|
||||||
|
return typeof props.bodyRowClass === 'function'
|
||||||
|
? props.bodyRowClass(row)
|
||||||
|
: props.bodyRowClass;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('overflow-x-auto rounded-xl border border-gray-200 bg-white', wrapperClass)">
|
||||||
|
<table :class="cn('w-full min-w-[48rem] border-collapse', tableClass)">
|
||||||
|
<thead class="bg-header">
|
||||||
|
<tr
|
||||||
|
v-for="headerGroup in table.getHeaderGroups()"
|
||||||
|
:key="headerGroup.id"
|
||||||
|
:class="cn('border-b border-gray-200', headerRowClass)"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
:class="cn(
|
||||||
|
'px-4 py-3 text-left text-sm font-medium text-gray-600',
|
||||||
|
header.column.getCanSort() && !header.isPlaceholder && 'cursor-pointer select-none',
|
||||||
|
(header.column.columnDef.meta as TableColumnMeta | undefined)?.headerClass
|
||||||
|
)"
|
||||||
|
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<FlexRender
|
||||||
|
v-if="!header.isPlaceholder"
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="header.column.getCanSort()"
|
||||||
|
class="text-[10px] uppercase tracking-wide text-gray-400"
|
||||||
|
>
|
||||||
|
{{ header.column.getIsSorted() === 'asc' ? 'asc' : header.column.getIsSorted() === 'desc' ? 'desc' : '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loading">
|
||||||
|
<td
|
||||||
|
:colspan="columns.length || 1"
|
||||||
|
class="px-4 py-10 text-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
<slot name="loading">
|
||||||
|
Loading...
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-else-if="!table.getRowModel().rows.length">
|
||||||
|
<td
|
||||||
|
:colspan="columns.length || 1"
|
||||||
|
class="px-4 py-10 text-center text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
<slot name="empty">
|
||||||
|
{{ emptyText }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
v-for="row in table.getRowModel().rows"
|
||||||
|
v-else
|
||||||
|
:key="row.id"
|
||||||
|
:class="cn(
|
||||||
|
'border-b border-gray-200 transition-colors last:border-b-0 hover:bg-gray-50',
|
||||||
|
resolveBodyRowClass(row)
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
:class="cn(
|
||||||
|
'px-4 py-3 align-middle',
|
||||||
|
(cell.column.columnDef.meta as TableColumnMeta | undefined)?.cellClass
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<FlexRender
|
||||||
|
:render="cell.column.columnDef.cell"
|
||||||
|
:props="cell.getContext()"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
src/composables/useAdminRuntimeMqtt.ts
Normal file
58
src/composables/useAdminRuntimeMqtt.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { TinyMqttClient } from "@/lib/liteMqtt";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { computed, onBeforeUnmount, watch } from "vue";
|
||||||
|
|
||||||
|
type RuntimeMessage = {
|
||||||
|
topic: string;
|
||||||
|
payload: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mqttBrokerUrl = "wss://mqtt-dashboard.com:8884/mqtt";
|
||||||
|
|
||||||
|
export function useAdminRuntimeMqtt(onMessage: (message: RuntimeMessage) => void) {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
let client: TinyMqttClient | undefined;
|
||||||
|
|
||||||
|
const isAdmin = computed(() => auth.user?.role?.toUpperCase?.() === "ADMIN");
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
if (import.meta.env.SSR || !isAdmin.value) return;
|
||||||
|
disconnect();
|
||||||
|
client = new TinyMqttClient(
|
||||||
|
mqttBrokerUrl,
|
||||||
|
["picpic/events", "picpic/logs/#", "picpic/job/+"],
|
||||||
|
(topic, raw) => {
|
||||||
|
try {
|
||||||
|
onMessage({ topic, payload: JSON.parse(raw) });
|
||||||
|
} catch {
|
||||||
|
onMessage({ topic, payload: raw });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
client.connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
client?.disconnect();
|
||||||
|
client = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopWatch = watch(
|
||||||
|
() => [auth.user?.id, auth.user?.role],
|
||||||
|
() => {
|
||||||
|
if (isAdmin.value) {
|
||||||
|
connect();
|
||||||
|
} else {
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopWatch();
|
||||||
|
disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { disconnect };
|
||||||
|
}
|
||||||
@@ -30,12 +30,16 @@ const state = reactive<AppConfirmState>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requireConfirm = (options: AppConfirmOptions) => {
|
const requireConfirm = (options: AppConfirmOptions) => {
|
||||||
|
const defaultHeader = 'Confirm';
|
||||||
|
const defaultAccept = 'OK';
|
||||||
|
const defaultReject = 'Cancel';
|
||||||
|
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
state.message = options.message;
|
state.message = options.message;
|
||||||
state.header = options.header ?? 'Confirm';
|
state.header = options.header ?? defaultHeader;
|
||||||
state.acceptLabel = options.acceptLabel ?? 'OK';
|
state.acceptLabel = options.acceptLabel ?? defaultAccept;
|
||||||
state.rejectLabel = options.rejectLabel ?? 'Cancel';
|
state.rejectLabel = options.rejectLabel ?? defaultReject;
|
||||||
state.accept = options.accept;
|
state.accept = options.accept;
|
||||||
state.reject = options.reject;
|
state.reject = options.reject;
|
||||||
};
|
};
|
||||||
|
|||||||
47
src/composables/useNetworkStatus.ts
Normal file
47
src/composables/useNetworkStatus.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isOffline = ref(false)
|
||||||
|
|
||||||
|
let listenersCount = 0
|
||||||
|
|
||||||
|
function syncNetworkStatus() {
|
||||||
|
if (typeof navigator === 'undefined') return
|
||||||
|
|
||||||
|
isOffline.value = !navigator.onLine
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNetworkStatusChange() {
|
||||||
|
syncNetworkStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startListening() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
if (listenersCount === 0) {
|
||||||
|
syncNetworkStatus()
|
||||||
|
window.addEventListener('online', handleNetworkStatusChange)
|
||||||
|
window.addEventListener('offline', handleNetworkStatusChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
listenersCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopListening() {
|
||||||
|
if (typeof window === 'undefined' || listenersCount === 0) return
|
||||||
|
|
||||||
|
listenersCount -= 1
|
||||||
|
|
||||||
|
if (listenersCount === 0) {
|
||||||
|
window.removeEventListener('online', handleNetworkStatusChange)
|
||||||
|
window.removeEventListener('offline', handleNetworkStatusChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNetworkStatus() {
|
||||||
|
return {
|
||||||
|
isOffline,
|
||||||
|
syncNetworkStatus,
|
||||||
|
startListening,
|
||||||
|
stopListening,
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/composables/useNotifications.ts
Normal file
126
src/composables/useNotifications.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
|
export type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
|
||||||
|
|
||||||
|
export type AppNotification = {
|
||||||
|
id: string;
|
||||||
|
type: NotificationType;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
time: string;
|
||||||
|
read: boolean;
|
||||||
|
actionUrl?: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NotificationApiItem = {
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
read?: boolean;
|
||||||
|
actionUrl?: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifications = ref<AppNotification[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const loaded = ref(false);
|
||||||
|
|
||||||
|
const normalizeType = (value?: string): NotificationType => {
|
||||||
|
switch ((value || '').toLowerCase()) {
|
||||||
|
case 'video':
|
||||||
|
case 'payment':
|
||||||
|
case 'warning':
|
||||||
|
case 'error':
|
||||||
|
case 'success':
|
||||||
|
case 'system':
|
||||||
|
return value as NotificationType;
|
||||||
|
default:
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNotifications() {
|
||||||
|
const { t, i18next } = useTranslation();
|
||||||
|
|
||||||
|
const formatRelativeTime = (value?: string) => {
|
||||||
|
if (!value) return '';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
|
||||||
|
const diffMs = Date.now() - date.getTime();
|
||||||
|
const minutes = Math.max(1, Math.floor(diffMs / 60000));
|
||||||
|
if (minutes < 60) return t('notification.time.minutesAgo', { count: minutes });
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return t('notification.time.hoursAgo', { count: hours });
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
return t('notification.time.daysAgo', { count: Math.max(1, days) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapNotification = (item: NotificationApiItem): AppNotification => ({
|
||||||
|
id: item.id || '',
|
||||||
|
type: normalizeType(item.type),
|
||||||
|
title: item.title || '',
|
||||||
|
message: item.message || '',
|
||||||
|
time: formatRelativeTime(item.createdAt),
|
||||||
|
read: Boolean(item.read),
|
||||||
|
actionUrl: item.actionUrl || undefined,
|
||||||
|
actionLabel: item.actionLabel || undefined,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchNotifications = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await rpcClient.listNotifications();
|
||||||
|
notifications.value = (response.notifications || []).map(mapNotification);
|
||||||
|
loaded.value = true;
|
||||||
|
return notifications.value;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markRead = async (id: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
await rpcClient.markNotificationRead({ id });
|
||||||
|
const item = notifications.value.find(notification => notification.id === id);
|
||||||
|
if (item) item.read = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNotification = async (id: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
await rpcClient.deleteNotification({ id });
|
||||||
|
notifications.value = notifications.value.filter(notification => notification.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllRead = async () => {
|
||||||
|
await rpcClient.markAllNotificationsRead();
|
||||||
|
notifications.value = notifications.value.map(item => ({ ...item, read: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAll = async () => {
|
||||||
|
await rpcClient.clearNotifications();
|
||||||
|
notifications.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = computed(() => notifications.value.filter(item => !item.read).length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
unreadCount,
|
||||||
|
locale: computed(() => i18next.resolvedLanguage),
|
||||||
|
fetchNotifications,
|
||||||
|
markRead,
|
||||||
|
deleteNotification,
|
||||||
|
markAllRead,
|
||||||
|
clearAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
59
src/composables/useRouteLoading.ts
Normal file
59
src/composables/useRouteLoading.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const progress = ref(0)
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
|
||||||
|
visible.value = true
|
||||||
|
progress.value = 8
|
||||||
|
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (progress.value < 80) {
|
||||||
|
progress.value += Math.random() * 12
|
||||||
|
} else if (progress.value < 95) {
|
||||||
|
progress.value += Math.random() * 3
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.value = 100
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
progress.value = 0
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.value = 100
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false
|
||||||
|
progress.value = 0
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRouteLoading() {
|
||||||
|
return {
|
||||||
|
visible,
|
||||||
|
progress,
|
||||||
|
start,
|
||||||
|
finish,
|
||||||
|
fail,
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/composables/useSettingsPreferencesQuery.ts
Normal file
76
src/composables/useSettingsPreferencesQuery.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
|
import type { Preferences } from '@/server/gen/proto/app/v1/common';
|
||||||
|
import type { UpdatePreferencesRequest } from '@/server/gen/proto/app/v1/account';
|
||||||
|
import { useQuery } from '@pinia/colada';
|
||||||
|
|
||||||
|
export const SETTINGS_PREFERENCES_QUERY_KEY = ['settings', 'preferences'] as const;
|
||||||
|
|
||||||
|
export type SettingsPreferencesSnapshot = {
|
||||||
|
emailNotifications: boolean;
|
||||||
|
pushNotifications: boolean;
|
||||||
|
marketingNotifications: boolean;
|
||||||
|
telegramNotifications: boolean;
|
||||||
|
language: string;
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NotificationSettingsDraft = {
|
||||||
|
email: boolean;
|
||||||
|
push: boolean;
|
||||||
|
marketing: boolean;
|
||||||
|
telegram: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreferencesResponse = {
|
||||||
|
preferences?: Preferences;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT: SettingsPreferencesSnapshot = {
|
||||||
|
emailNotifications: true,
|
||||||
|
pushNotifications: true,
|
||||||
|
marketingNotifications: false,
|
||||||
|
telegramNotifications: false,
|
||||||
|
language: 'en',
|
||||||
|
locale: 'en',
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePreferencesSnapshot = (responseData: unknown): SettingsPreferencesSnapshot => {
|
||||||
|
const preferences = (responseData as PreferencesResponse | undefined)?.preferences;
|
||||||
|
|
||||||
|
return {
|
||||||
|
emailNotifications: preferences?.emailNotifications ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.emailNotifications,
|
||||||
|
pushNotifications: preferences?.pushNotifications ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.pushNotifications,
|
||||||
|
marketingNotifications: preferences?.marketingNotifications ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.marketingNotifications,
|
||||||
|
telegramNotifications: preferences?.telegramNotifications ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.telegramNotifications,
|
||||||
|
language: preferences?.language ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.language,
|
||||||
|
locale: preferences?.locale ?? DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT.locale,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNotificationSettingsDraft = (
|
||||||
|
snapshot: SettingsPreferencesSnapshot = DEFAULT_SETTINGS_PREFERENCES_SNAPSHOT,
|
||||||
|
): NotificationSettingsDraft => ({
|
||||||
|
email: snapshot.emailNotifications,
|
||||||
|
push: snapshot.pushNotifications,
|
||||||
|
marketing: snapshot.marketingNotifications,
|
||||||
|
telegram: snapshot.telegramNotifications,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toNotificationPreferencesPayload = (
|
||||||
|
draft: NotificationSettingsDraft,
|
||||||
|
): UpdatePreferencesRequest => ({
|
||||||
|
emailNotifications: draft.email,
|
||||||
|
pushNotifications: draft.push,
|
||||||
|
marketingNotifications: draft.marketing,
|
||||||
|
telegramNotifications: draft.telegram,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useSettingsPreferencesQuery() {
|
||||||
|
return useQuery({
|
||||||
|
key: () => SETTINGS_PREFERENCES_QUERY_KEY,
|
||||||
|
query: async () => {
|
||||||
|
const response = await rpcClient.getPreferences();
|
||||||
|
return normalizePreferencesSnapshot(response);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { client } from '@/api/rpcclient';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
export interface QueueItem {
|
export interface QueueItem {
|
||||||
@@ -282,22 +283,19 @@ export function useUploadQueue() {
|
|||||||
if (!item.file || !item.uploadedUrls) return;
|
if (!item.file || !item.uploadedUrls) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/merge', {
|
const data = await client.merge(item.file.name, item.uploadedUrls, item.file.size);
|
||||||
method: 'POST',
|
// const response = await fetch('/merge', {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// method: 'POST',
|
||||||
body: JSON.stringify({
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
filename: item.file.name,
|
// body: JSON.stringify({
|
||||||
chunks: item.uploadedUrls,
|
// filename: item.file.name,
|
||||||
size: item.file.size
|
// chunks: item.uploadedUrls,
|
||||||
})
|
// size: item.file.size
|
||||||
});
|
// })
|
||||||
|
// });
|
||||||
const data = await response.json();
|
if (!data) {
|
||||||
|
throw new Error('No response from server');
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || 'Merge failed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.status = 'complete';
|
item.status = 'complete';
|
||||||
item.progress = 100;
|
item.progress = 100;
|
||||||
item.uploaded = item.total;
|
item.uploaded = item.total;
|
||||||
@@ -370,4 +368,4 @@ export function useUploadQueue() {
|
|||||||
remainingSlots,
|
remainingSlots,
|
||||||
maxItems: MAX_ITEMS,
|
maxItems: MAX_ITEMS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
38
src/composables/useUsageQuery.ts
Normal file
38
src/composables/useUsageQuery.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
|
import { useQuery } from '@pinia/colada';
|
||||||
|
|
||||||
|
export const USAGE_QUERY_KEY = ['usage'] as const;
|
||||||
|
|
||||||
|
export type UsageSnapshot = {
|
||||||
|
totalVideos: number;
|
||||||
|
totalStorage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsageResponse = {
|
||||||
|
totalVideos?: number;
|
||||||
|
totalStorage?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_USAGE_SNAPSHOT: UsageSnapshot = {
|
||||||
|
totalVideos: 0,
|
||||||
|
totalStorage: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeUsageSnapshot = (responseData: unknown): UsageSnapshot => {
|
||||||
|
const usage = responseData as UsageResponse | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalVideos: usage?.totalVideos ?? DEFAULT_USAGE_SNAPSHOT.totalVideos,
|
||||||
|
totalStorage: usage?.totalStorage ?? DEFAULT_USAGE_SNAPSHOT.totalStorage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useUsageQuery() {
|
||||||
|
return useQuery({
|
||||||
|
key: () => USAGE_QUERY_KEY,
|
||||||
|
query: async () => {
|
||||||
|
const response = await rpcClient.getUsage();
|
||||||
|
return normalizeUsageSnapshot(response);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
7
src/i18n/constants.ts
Normal file
7
src/i18n/constants.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const supportedLocales = ['en', 'vi'] as const;
|
||||||
|
|
||||||
|
export type SupportedLocale = (typeof supportedLocales)[number];
|
||||||
|
|
||||||
|
export const defaultLocale: SupportedLocale = 'en';
|
||||||
|
|
||||||
|
export const localeCookieKey = 'i18next';
|
||||||
@@ -1,26 +1,19 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
import { apiProxyMiddleware } from './server/middlewares/apiProxy';
|
|
||||||
import { setupMiddlewares } from './server/middlewares/setup';
|
import { setupMiddlewares } from './server/middlewares/setup';
|
||||||
import { registerDisplayRoutes } from './server/routes/display';
|
import { registerAuthRoutes } from './server/routes/auth';
|
||||||
import { registerManifestRoutes } from './server/routes/manifest';
|
import { registerRpcRoutes } from './server/routes/rpc';
|
||||||
import { registerMergeRoutes } from './server/routes/merge';
|
|
||||||
import { registerSSRRoutes } from './server/routes/ssr';
|
import { registerSSRRoutes } from './server/routes/ssr';
|
||||||
import { registerWellKnownRoutes } from './server/routes/wellKnown';
|
import { registerWellKnownRoutes } from './server/routes/wellKnown';
|
||||||
|
import { setupServices } from './server/services/grpcClient';
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
// Global middlewares
|
// Global middlewares
|
||||||
setupMiddlewares(app);
|
setupMiddlewares(app);
|
||||||
|
setupServices(app);
|
||||||
// API proxy middleware (handles /r/*)
|
|
||||||
app.use(apiProxyMiddleware);
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
registerWellKnownRoutes(app);
|
registerWellKnownRoutes(app);
|
||||||
registerMergeRoutes(app);
|
registerAuthRoutes(app);
|
||||||
registerDisplayRoutes(app);
|
registerRpcRoutes(app);
|
||||||
registerManifestRoutes(app);
|
|
||||||
registerSSRRoutes(app);
|
registerSSRRoutes(app);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
42
src/lib/translation/index.ts
Normal file
42
src/lib/translation/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import i18next from "i18next";
|
||||||
|
import I18NextHttpBackend, { HttpBackendOptions } from "i18next-http-backend";
|
||||||
|
const backendOptions: HttpBackendOptions = {
|
||||||
|
loadPath: 'http://localhost:5173/locales/{{lng}}/{{ns}}.json',
|
||||||
|
request: (_options, url, _payload, callback) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((res) =>
|
||||||
|
res.json().then((r) => {
|
||||||
|
callback(null, {
|
||||||
|
data: JSON.stringify(r),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
callback(null, {
|
||||||
|
status: 500,
|
||||||
|
data: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const createI18nInstance = (lng: string) => {
|
||||||
|
console.log('Initializing i18n with language:', lng);
|
||||||
|
const i18n = i18next.createInstance();
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(I18NextHttpBackend)
|
||||||
|
.init({
|
||||||
|
lng,
|
||||||
|
supportedLngs: ["en", "vi"],
|
||||||
|
fallbackLng: "en",
|
||||||
|
defaultNS: "translation",
|
||||||
|
ns: ['translation'],
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
backend: backendOptions,
|
||||||
|
});
|
||||||
|
return i18n;
|
||||||
|
};
|
||||||
|
export default createI18nInstance;
|
||||||
@@ -49,14 +49,14 @@ export function getImageAspectRatio(url: string): Promise<AspectInfo> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const formatBytes = (bytes?: number) => {
|
export const formatBytes = (bytes?: number) => {
|
||||||
if (!bytes) return '0 B';
|
if (!bytes) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
|
||||||
|
return `${value} ${sizes[i]}`;
|
||||||
|
// return `${new Intl.NumberFormat(getRuntimeLocaleTag()).format(value)} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDuration = (seconds?: number) => {
|
export const formatDuration = (seconds?: number) => {
|
||||||
@@ -73,7 +73,10 @@ export const formatDuration = (seconds?: number) => {
|
|||||||
|
|
||||||
export const formatDate = (dateString: string = "", dateOnly: boolean = false) => {
|
export const formatDate = (dateString: string = "", dateOnly: boolean = false) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
const locale = typeof document !== 'undefined'
|
||||||
|
? document.documentElement.lang === 'vi' ? 'vi-VN' : 'en-US'
|
||||||
|
: 'en-US';
|
||||||
|
return new Date(dateString).toLocaleDateString(locale, {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -93,4 +96,8 @@ export const getStatusSeverity = (status: string = "") => {
|
|||||||
default:
|
default:
|
||||||
return 'info';
|
return 'info';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
export const isAdmin = (role: string = "") => {
|
||||||
|
const r = String(role).toLowerCase();
|
||||||
|
return r === "admin" || r === "superadmin";
|
||||||
};
|
};
|
||||||
41
src/main.ts
41
src/main.ts
@@ -1,17 +1,29 @@
|
|||||||
import { PiniaColada, useQueryCache } from '@pinia/colada';
|
import { PiniaColada, useQueryCache } from '@pinia/colada';
|
||||||
import { createHead as CSRHead } from "@unhead/vue/client";
|
import { createHead as CSRHead } from '@unhead/vue/client';
|
||||||
import { createHead as SSRHead } from "@unhead/vue/server";
|
import { createHead as SSRHead } from '@unhead/vue/server';
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from 'pinia';
|
||||||
import { createSSRApp } from 'vue';
|
import { createSSRApp } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
|
import I18NextVue from 'i18next-vue';
|
||||||
|
|
||||||
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
||||||
|
import createI18nInstance from './lib/translation';
|
||||||
import createAppRouter from './routes';
|
import createAppRouter from './routes';
|
||||||
|
|
||||||
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen"
|
const bodyClass = ':uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen';
|
||||||
export function createApp() {
|
|
||||||
|
const getSerializedAppData = () => {
|
||||||
|
if (typeof document === 'undefined') return {} as Record<string, any>;
|
||||||
|
return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createApp(lng: string = 'en') {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createSSRApp(withErrorBoundary(RouterView));
|
const app = createSSRApp(withErrorBoundary(RouterView));
|
||||||
|
|
||||||
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
|
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
|
||||||
|
const appData = !import.meta.env.SSR ? getSerializedAppData() : ({} as Record<string, any>);
|
||||||
|
|
||||||
app.use(head);
|
app.use(head);
|
||||||
app.directive('nh', {
|
app.directive('nh', {
|
||||||
@@ -20,11 +32,12 @@ export function createApp() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
|
app.use(I18NextVue, { i18next: createI18nInstance(lng) });
|
||||||
app.use(PiniaColada, {
|
app.use(PiniaColada, {
|
||||||
pinia,
|
pinia,
|
||||||
plugins: [
|
plugins: [
|
||||||
(context) => {
|
() => {
|
||||||
// console.log("PiniaColada plugin initialized for store:", context);
|
// reserved for query plugins
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
queryOptions: {
|
queryOptions: {
|
||||||
@@ -32,19 +45,21 @@ export function createApp() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
ssrCatchError: true,
|
ssrCatchError: true,
|
||||||
}
|
}
|
||||||
// optional options
|
});
|
||||||
})
|
|
||||||
// app.use(vueSWR({ revalidateOnFocus: false }));
|
|
||||||
const queryCache = useQueryCache();
|
const queryCache = useQueryCache();
|
||||||
const router = createAppRouter();
|
|
||||||
app.use(router);
|
|
||||||
if (!import.meta.env.SSR) {
|
if (!import.meta.env.SSR) {
|
||||||
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
|
Object.entries(appData).forEach(([key, value]) => {
|
||||||
(window as any)[key] = value;
|
(window as any)[key] = value;
|
||||||
});
|
});
|
||||||
if ((window as any).$p) {
|
if ((window as any).$p) {
|
||||||
pinia.state.value = (window as any).$p;
|
pinia.state.value = (window as any).$p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = createAppRouter();
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
return { app, router, head, pinia, bodyClass, queryCache };
|
return { app, router, head, pinia, bodyClass, queryCache };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ModelVideo } from "@/api/client";
|
import type { Video as ModelVideo } from "@/server/gen/proto/app/v1/common";
|
||||||
|
|
||||||
export const mockVideos: ModelVideo[] = [
|
export const mockVideos: ModelVideo[] = [
|
||||||
{
|
{
|
||||||
@@ -9,7 +9,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 345, // 5m 45s
|
duration: 345, // 5m 45s
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 45, // 45MB
|
size: 1024 * 1024 * 45, // 45MB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(), // 2 days ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(), // 2 days ago
|
||||||
views: 12500,
|
views: 12500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -20,9 +20,8 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
thumbnail: 'https://picsum.photos/seed/video2/640/360',
|
thumbnail: 'https://picsum.photos/seed/video2/640/360',
|
||||||
duration: 890, // 14m 50s
|
duration: 890, // 14m 50s
|
||||||
status: 'processing',
|
status: 'processing',
|
||||||
processing_status: '75%',
|
|
||||||
size: 1024 * 1024 * 128, // 128MB
|
size: 1024 * 1024 * 128, // 128MB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(), // 5 hours ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(), // 5 hours ago
|
||||||
views: 0,
|
views: 0,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -34,7 +33,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 120, // 2m 00s
|
duration: 120, // 2m 00s
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 25, // 25MB
|
size: 1024 * 1024 * 25, // 25MB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(), // 1 week ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(), // 1 week ago
|
||||||
views: 340,
|
views: 340,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -46,7 +45,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 1800, // 30m 00s
|
duration: 1800, // 30m 00s
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 350, // 350MB
|
size: 1024 * 1024 * 350, // 350MB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14).toISOString(), // 2 weeks ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14).toISOString(), // 2 weeks ago
|
||||||
views: 12,
|
views: 12,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -58,7 +57,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 600, // 10m 00s
|
duration: 600, // 10m 00s
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
size: 1024 * 1024 * 80, // 80MB
|
size: 1024 * 1024 * 80, // 80MB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(), // 30 mins ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(), // 30 mins ago
|
||||||
views: 0,
|
views: 0,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -70,7 +69,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 5400, // 1h 30m
|
duration: 5400, // 1h 30m
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 1024 * 2.5, // 2.5GB
|
size: 1024 * 1024 * 1024 * 2.5, // 2.5GB
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(), // 1 month ago
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toISOString(), // 1 month ago
|
||||||
views: 45000,
|
views: 45000,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -82,7 +81,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 1540,
|
duration: 1540,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 200,
|
size: 1024 * 1024 * 200,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(),
|
||||||
views: 8900,
|
views: 8900,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -94,7 +93,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -106,7 +105,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -118,7 +117,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -130,7 +129,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -142,7 +141,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -154,7 +153,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -166,7 +165,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -178,7 +177,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -190,7 +189,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -202,7 +201,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -214,7 +213,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -226,7 +225,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -238,7 +237,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -250,7 +249,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -262,7 +261,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -274,7 +273,7 @@ export const mockVideos: ModelVideo[] = [
|
|||||||
duration: 3200,
|
duration: 3200,
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
size: 1024 * 1024 * 800,
|
size: 1024 * 1024 * 800,
|
||||||
created_at: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
||||||
views: 1500,
|
views: 1500,
|
||||||
url: '#'
|
url: '#'
|
||||||
},
|
},
|
||||||
@@ -340,7 +339,7 @@ export const updateMockVideo = async (id: string, updates: { title: string; desc
|
|||||||
...mockVideos[videoIndex],
|
...mockVideos[videoIndex],
|
||||||
title: updates.title,
|
title: updates.title,
|
||||||
description: updates.description,
|
description: updates.description,
|
||||||
updated_at: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
return mockVideos[videoIndex];
|
return mockVideos[videoIndex];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<vue-head :input="{title: '404 - Page Not Found'}"/>
|
<vue-head :input="{ title: t('notFound.headTitle') }" />
|
||||||
<div class="mx-auto text-center mt-20 flex flex-col items-center gap-4">
|
<div class="mx-auto text-center mt-20 flex flex-col items-center gap-4">
|
||||||
<h1>404 - Page Not Found</h1>
|
<h1>{{ t('notFound.title') }}</h1>
|
||||||
<p>The page you are looking for does not exist.</p>
|
<p>{{ t('notFound.description') }}</p>
|
||||||
<router-link class="btn btn-primary" to="/">Go back to Home</router-link>
|
<router-link class="btn btn-primary" to="/">{{ t('notFound.backHome') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueHead } from "@/components/VueHead";
|
import { VueHead } from '@/components/VueHead';
|
||||||
</script>
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
||||||
<div class="text-sm text-gray-600 mb-2">
|
<div class="text-sm text-gray-600 mb-2">
|
||||||
Enter your email address and we'll send you a link to reset your password.
|
{{ t('auth.forgot.description') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
<label for="email" class="text-sm font-medium text-gray-700">{{ t('auth.forgot.email') }}</label>
|
||||||
<AppInput id="email" v-model="form.email" type="email" placeholder="you@example.com" />
|
<AppInput id="email" v-model="form.email" type="email" :placeholder="t('auth.forgot.placeholders.email')" />
|
||||||
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppButton type="submit" class="w-full">Send Reset Link</AppButton>
|
<AppButton type="submit" class="w-full">{{ t('auth.forgot.sendResetLink') }}</AppButton>
|
||||||
|
|
||||||
<div class="text-center mt-2">
|
<div class="text-center mt-2">
|
||||||
<router-link to="/login" replace
|
<router-link to="/login" replace
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
Back to Sign in
|
{{ t('auth.forgot.backToSignIn') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -28,12 +28,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { client } from '@/api/client';
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
import { useAppToast } from '@/composables/useAppToast';
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const toast = useAppToast();
|
const toast = useAppToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
email: ''
|
email: ''
|
||||||
@@ -42,7 +44,7 @@ const form = reactive({
|
|||||||
const errors = reactive<{ email?: string }>({});
|
const errors = reactive<{ email?: string }>({});
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' })
|
email: z.string().min(1, { message: t('auth.forgot.errors.emailRequired') }).email({ message: t('auth.forgot.errors.emailInvalid') })
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = () => {
|
const onFormSubmit = () => {
|
||||||
@@ -57,12 +59,22 @@ const onFormSubmit = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.auth.forgotPasswordCreate({ email: form.email })
|
rpcClient.forgotPassword({ email: form.email })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('auth.forgot.toast.successSummary'),
|
||||||
|
detail: t('auth.forgot.toast.successDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: error.message || 'An error occurred', life: 3000 });
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('auth.forgot.toast.errorSummary'),
|
||||||
|
detail: error.message || t('auth.forgot.toast.errorDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
69
src/routes/auth/google-finalize.vue
Normal file
69
src/routes/auth/google-finalize.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center gap-3 py-6 text-center">
|
||||||
|
<div class="i-svg-spinners-90-ring-with-bg h-10 w-10 text-blue-600"></div>
|
||||||
|
<p class="text-sm text-gray-600">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const toast = useAppToast();
|
||||||
|
|
||||||
|
const status = computed(() => String(route.query.status ?? 'error'));
|
||||||
|
const reason = computed(() => String(route.query.reason ?? 'google_login_failed'));
|
||||||
|
|
||||||
|
const reasonMessages: Record<string, string> = {
|
||||||
|
missing_code: 'Google did not return an authorization code.',
|
||||||
|
access_denied: 'Google login was cancelled.',
|
||||||
|
exchange_failed: 'Failed to verify your Google sign-in. Please try again.',
|
||||||
|
userinfo_failed: 'Failed to load your Google account information.',
|
||||||
|
userinfo_parse_failed: 'Failed to read your Google account information.',
|
||||||
|
missing_email: 'Your Google account did not provide an email address.',
|
||||||
|
create_user_failed: 'Failed to create your account.',
|
||||||
|
update_user_failed: 'Failed to update your account.',
|
||||||
|
reload_user_failed: 'Failed to finish signing you in.',
|
||||||
|
session_failed: 'Failed to create your sign-in session.',
|
||||||
|
fetch_me_failed: 'Signed in with Google, but failed to load your account.',
|
||||||
|
google_login_failed: 'Google login failed. Please try again.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorMessage = computed(() => reasonMessages[reason.value] ?? reasonMessages.google_login_failed);
|
||||||
|
const message = computed(() => status.value === 'success' ? 'Signing you in with Google...' : errorMessage.value);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (status.value !== 'success') {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Google login failed',
|
||||||
|
detail: errorMessage.value,
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
await router.replace({ name: 'login', query: { reason: reason.value } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await auth.fetchMe();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('missing_user');
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.replace({ name: 'overview' });
|
||||||
|
} catch {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Google login failed',
|
||||||
|
detail: 'Signed in with Google, but failed to load your account.',
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
await router.replace({ name: 'login', query: { reason: 'fetch_me_failed' } });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -5,12 +5,12 @@
|
|||||||
class=":uno: w-full shadow-xl bg-white p-6 rounded-xl relative before:(content-[''] absolute inset-[-5px] translate-0 z-[-1] opacity-50 rounded-xl bg-[linear-gradient(135deg,var(--glow-stop-1)_0,var(--glow-stop-2)_25%,var(--glow-stop-3)_50%,var(--glow-stop-4)_75%,var(--glow-stop-5)_100%)] animate-[glow-enter-blur_1s_ease_.5s_both]) after:(content-[''] absolute inset-[-1px] translate-0 z-[-1] opacity-50 rounded-xl bg-[linear-gradient(135deg,transparent_0,transparent_34%,transparent_49%,#fff_57%,#fff_64%,var(--glow-stop-1)_66%,var(--glow-stop-2)_75%,var(--glow-stop-3)_83%,var(--glow-stop-4)_92%,var(--glow-stop-5)_100%)] bg-[length:300%_300%] bg-[position:0_0] bg-no-repeat transition-background-position duration-800 ease animate-[glow-enter-stroke_.5s_ease_.5s_both])">
|
class=":uno: w-full shadow-xl bg-white p-6 rounded-xl relative before:(content-[''] absolute inset-[-5px] translate-0 z-[-1] opacity-50 rounded-xl bg-[linear-gradient(135deg,var(--glow-stop-1)_0,var(--glow-stop-2)_25%,var(--glow-stop-3)_50%,var(--glow-stop-4)_75%,var(--glow-stop-5)_100%)] animate-[glow-enter-blur_1s_ease_.5s_both]) after:(content-[''] absolute inset-[-1px] translate-0 z-[-1] opacity-50 rounded-xl bg-[linear-gradient(135deg,transparent_0,transparent_34%,transparent_49%,#fff_57%,#fff_64%,var(--glow-stop-1)_66%,var(--glow-stop-2)_75%,var(--glow-stop-3)_83%,var(--glow-stop-4)_92%,var(--glow-stop-5)_100%)] bg-[length:300%_300%] bg-[position:0_0] bg-no-repeat transition-background-position duration-800 ease animate-[glow-enter-stroke_.5s_ease_.5s_both])">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-medium text-gray-900">
|
<h2 class="text-xl font-medium text-gray-900">
|
||||||
{{ content[route.name as keyof typeof content]?.title || '' }}
|
{{ content[route.name as keyof typeof content.value]?.title || '' }}
|
||||||
</h2>
|
</h2>
|
||||||
<vue-head :input="{
|
<vue-head :input="{
|
||||||
title: content[route.name as keyof typeof content]?.headTitle || 'Authentication',
|
title: content[route.name as keyof typeof content.value]?.headTitle || t('app.name'),
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: content[route.name as keyof typeof content]?.subtitle || '' }
|
{ name: 'description', content: content[route.name as keyof typeof content.value]?.subtitle || '' }
|
||||||
]
|
]
|
||||||
}" />
|
}" />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,29 +18,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<router-link to="/" class="inline-flex items-center justify-center w-6 h-6 mt-10 group w-full">
|
<router-link to="/" class="inline-flex items-center justify-center w-6 h-6 mt-10 group w-full">
|
||||||
<img class="w-6 h-6" src="/apple-touch-icon.png" alt="Logo" /> <span
|
<img class="w-6 h-6" src="/apple-touch-icon.png" alt="Logo" /> <span
|
||||||
class="text-[#6a6a6a] font-medium group-hover:text-gray-900">EcoStream</span>
|
class="text-[#6a6a6a] font-medium group-hover:text-gray-900">{{ t('app.name') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const content = {
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const content = computed(() => ({
|
||||||
login: {
|
login: {
|
||||||
headTitle: "Login to your account",
|
headTitle: t('auth.layout.login.headTitle'),
|
||||||
title: 'Sign in to your dashboard',
|
title: t('auth.layout.login.title'),
|
||||||
subtitle: 'Please enter your details to sign in.'
|
subtitle: t('auth.layout.login.subtitle')
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
headTitle: "Create your account",
|
headTitle: t('auth.layout.signup.headTitle'),
|
||||||
title: 'Create your account',
|
title: t('auth.layout.signup.title'),
|
||||||
subtitle: 'Please fill in the information to create your account.'
|
subtitle: t('auth.layout.signup.subtitle')
|
||||||
},
|
},
|
||||||
forgot: {
|
forgot: {
|
||||||
title: 'Forgot your password?',
|
title: t('auth.layout.forgot.title'),
|
||||||
subtitle: "Enter your email address and we'll send you a link to reset your password.",
|
subtitle: t('auth.layout.forgot.subtitle'),
|
||||||
headTitle: "Reset your password"
|
headTitle: t('auth.layout.forgot.headTitle')
|
||||||
|
},
|
||||||
|
'google-auth-finalize': {
|
||||||
|
title: 'Google sign in',
|
||||||
|
subtitle: 'Completing your Google sign in.',
|
||||||
|
headTitle: 'Google sign in - Holistream'
|
||||||
}
|
}
|
||||||
}
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
|
<label for="email" class="text-sm font-medium text-gray-700">{{ t('auth.login.email') }}</label>
|
||||||
<AppInput id="email" v-model="form.email" type="text" placeholder="Enter your email"
|
<AppInput id="email" v-model="form.email" type="text" :placeholder="t('auth.signup.placeholders.email')"
|
||||||
:disabled="auth.loading" />
|
:disabled="auth.loading" />
|
||||||
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
<label for="password" class="text-sm font-medium text-gray-700">{{ t('auth.login.password') }}</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<AppInput id="password" v-model="form.password" :type="showPassword ? 'text' : 'password'"
|
<AppInput id="password" v-model="form.password" :type="showPassword ? 'text' : 'password'"
|
||||||
placeholder="Enter your password" :disabled="auth.loading" />
|
:placeholder="t('auth.signup.placeholders.password')" :disabled="auth.loading" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600"
|
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600"
|
||||||
@click="showPassword = !showPassword" tabindex="-1">
|
@click="showPassword = !showPassword" tabindex="-1">
|
||||||
@@ -31,22 +31,15 @@
|
|||||||
<p v-if="errors.password" class="text-xs text-red-500 mt-0.5">{{ errors.password }}</p>
|
<p v-if="errors.password" class="text-xs text-red-500 mt-0.5">{{ errors.password }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-end">
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input id="remember-me" v-model="form.rememberMe" type="checkbox"
|
|
||||||
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary"
|
|
||||||
:disabled="auth.loading" />
|
|
||||||
<label for="remember-me" class="text-sm text-gray-900">Remember me</label>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<router-link to="/forgot"
|
<router-link to="/forgot"
|
||||||
class="text-blue-600 hover:text-blue-500 hover:underline">Forgot
|
class="text-blue-600 hover:text-blue-500 hover:underline">{{ t('auth.login.forgotPassword') }}</router-link>
|
||||||
password?</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppButton type="submit" :loading="auth.loading" class="w-full">
|
<AppButton type="submit" :loading="auth.loading" class="w-full">
|
||||||
{{ auth.loading ? 'Signing in...' : 'Sign in' }}
|
{{ auth.loading ? `${t('common.loading')}...` : t('auth.login.signIn') }}
|
||||||
</AppButton>
|
</AppButton>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -54,7 +47,7 @@
|
|||||||
<div class="w-full border-t border-gray-300"></div>
|
<div class="w-full border-t border-gray-300"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex justify-center text-sm">
|
<div class="relative flex justify-center text-sm">
|
||||||
<span class="px-2 bg-white text-gray-500">Or continue with</span>
|
<span class="px-2 bg-white text-gray-500">{{ t('auth.login.google') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,13 +57,13 @@
|
|||||||
<path
|
<path
|
||||||
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
|
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
|
||||||
</svg>
|
</svg>
|
||||||
Google
|
{{ t('auth.login.google') }}
|
||||||
</AppButton>
|
</AppButton>
|
||||||
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
|
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
|
||||||
<p class="text-center text-sm text-gray-600">
|
<p class="text-center text-sm text-gray-600">
|
||||||
Don't have an account?
|
{{ t('auth.login.noAccount') }}
|
||||||
<router-link to="/sign-up"
|
<router-link to="/sign-up"
|
||||||
class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up</router-link>
|
class="font-medium text-blue-600 hover:text-blue-500 hover:underline">{{ t('auth.login.signUp') }}</router-link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -81,11 +74,13 @@
|
|||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useAppToast } from '@/composables/useAppToast';
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const toast = useAppToast();
|
const toast = useAppToast();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
@@ -96,8 +91,8 @@ const form = reactive({
|
|||||||
const errors = reactive<{ email?: string; password?: string }>({});
|
const errors = reactive<{ email?: string; password?: string }>({});
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().min(1, { message: 'Email or username is required.' }),
|
email: z.string().min(1, { message: t('auth.login.errors.emailRequired') }),
|
||||||
password: z.string().min(1, { message: 'Password is required.' })
|
password: z.string().min(1, { message: t('auth.login.errors.passwordRequired') })
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => auth.error, (newError) => {
|
watch(() => auth.error, (newError) => {
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
<form @submit.prevent="onFormSubmit" class="flex flex-col gap-4 w-full">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="name" class="text-sm font-medium text-gray-700">Full Name</label>
|
<label for="name" class="text-sm font-medium text-gray-700">{{ t('auth.signup.fullName') }}</label>
|
||||||
<AppInput id="name" v-model="form.name" placeholder="John Doe" />
|
<AppInput id="name" v-model="form.name" :placeholder="t('auth.signup.placeholders.name')" />
|
||||||
<p v-if="errors.name" class="text-xs text-red-500 mt-0.5">{{ errors.name }}</p>
|
<p v-if="errors.name" class="text-xs text-red-500 mt-0.5">{{ errors.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
<label for="email" class="text-sm font-medium text-gray-700">{{ t('auth.signup.email') }}</label>
|
||||||
<AppInput id="email" v-model="form.email" type="email" placeholder="you@example.com" />
|
<AppInput id="email" v-model="form.email" type="email" :placeholder="t('auth.signup.placeholders.email')" />
|
||||||
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
<p v-if="errors.email" class="text-xs text-red-500 mt-0.5">{{ errors.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
<label for="password" class="text-sm font-medium text-gray-700">{{ t('auth.signup.password') }}</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<AppInput id="password" v-model="form.password" :type="showPassword ? 'text' : 'password'"
|
<AppInput id="password" v-model="form.password" :type="showPassword ? 'text' : 'password'"
|
||||||
placeholder="Create a password" />
|
:placeholder="t('auth.signup.placeholders.password')" />
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600"
|
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600"
|
||||||
@click="showPassword = !showPassword" tabindex="-1">
|
@click="showPassword = !showPassword" tabindex="-1">
|
||||||
@@ -33,16 +33,36 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-gray-500">Must be at least 8 characters.</small>
|
<small class="text-gray-500">{{ t('auth.signup.passwordHint') }}</small>
|
||||||
<p v-if="errors.password" class="text-xs text-red-500 mt-0.5">{{ errors.password }}</p>
|
<p v-if="errors.password" class="text-xs text-red-500 mt-0.5">{{ errors.password }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppButton type="submit" class="w-full">Create Account</AppButton>
|
<div v-if="refUsername" class="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-700">
|
||||||
|
Signing up with referral: <span class="font-medium">@{{ refUsername }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppButton type="submit" class="w-full">{{ t('auth.signup.createAccount') }}</AppButton>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<div class="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-sm">
|
||||||
|
<span class="px-2 bg-white text-gray-500">{{ t('auth.login.google') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppButton type="button" variant="secondary" class="w-full flex items-center justify-center gap-2" @click="signupWithGoogle">
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
|
||||||
|
</svg>
|
||||||
|
Continue with Google
|
||||||
|
</AppButton>
|
||||||
|
|
||||||
<p class="mt-4 text-center text-sm text-gray-600">
|
<p class="mt-4 text-center text-sm text-gray-600">
|
||||||
Already have an account?
|
{{ t('auth.signup.alreadyHave') }}
|
||||||
<router-link to="/login"
|
<router-link to="/login"
|
||||||
class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign in</router-link>
|
class="font-medium text-blue-600 hover:text-blue-500 hover:underline">{{ t('auth.signup.signIn') }}</router-link>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,11 +70,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
const route = useRoute();
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const refUsername = computed(() => String(route.query.ref || '').trim());
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -65,9 +90,9 @@ const form = reactive({
|
|||||||
const errors = reactive<{ name?: string; email?: string; password?: string }>({});
|
const errors = reactive<{ name?: string; email?: string; password?: string }>({});
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().min(1, { message: 'Name is required.' }),
|
name: z.string().min(1, { message: t('auth.signup.errors.nameRequired') }),
|
||||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }),
|
email: z.string().min(1, { message: t('auth.signup.errors.emailRequired') }).email({ message: t('auth.signup.errors.emailInvalid') }),
|
||||||
password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
|
password: z.string().min(8, { message: t('auth.signup.errors.passwordMin') })
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = () => {
|
const onFormSubmit = () => {
|
||||||
@@ -84,6 +109,10 @@ const onFormSubmit = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.register(form.name, form.email, form.password);
|
auth.register(form.name, form.email, form.password, refUsername.value || undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const signupWithGoogle = () => {
|
||||||
|
auth.loginWithGoogle(refUsername.value || undefined);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,231 +1,373 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const signalItems = computed(() => [
|
||||||
|
{ label: t('home.features.live.bitrate'), value: t('home.features.live.bitrateValue') },
|
||||||
|
{ label: t('home.features.live.fps'), value: t('home.features.live.fpsValue') },
|
||||||
|
{ label: t('home.features.live.latency'), value: t('home.features.live.latencyValue') },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const featurePills = computed(() => [
|
||||||
|
t('home.features.global.title'),
|
||||||
|
t('home.features.encoding.title'),
|
||||||
|
t('home.features.analytics.title'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getFeatureList = (key: string): string[] => {
|
||||||
|
const localized = t(key, { returnObjects: true });
|
||||||
|
return Array.isArray(localized) ? localized.map((item) => String(item)) : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const pricing = computed(() => ({
|
||||||
|
title: t('home.pricing.title'),
|
||||||
|
subtitle: t('home.pricing.subtitle'),
|
||||||
|
packs: [
|
||||||
|
{
|
||||||
|
name: t('home.pricing.hobby.name'),
|
||||||
|
price: '$0',
|
||||||
|
features: getFeatureList('home.pricing.hobby.features'),
|
||||||
|
buttonText: t('home.pricing.hobby.button'),
|
||||||
|
tag: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('home.pricing.pro.name'),
|
||||||
|
price: '$29',
|
||||||
|
features: getFeatureList('home.pricing.pro.features'),
|
||||||
|
buttonText: t('home.pricing.pro.button'),
|
||||||
|
tag: t('home.pricing.pro.tag'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('home.pricing.scale.name'),
|
||||||
|
price: '$99',
|
||||||
|
features: getFeatureList('home.pricing.scale.features'),
|
||||||
|
buttonText: t('home.pricing.scale.button'),
|
||||||
|
tag: t('home.pricing.scale.tag'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const featuredTag = computed(() => t('home.pricing.pro.tag'));
|
||||||
|
const scaleTag = computed(() => t('home.pricing.scale.tag'));
|
||||||
|
const isFeaturedPack = (tag: string) => tag === featuredTag.value;
|
||||||
|
const isScalePack = (tag: string) => tag === scaleTag.value;
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class=":m: relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden min-h-svh flex">
|
<div class="bg-white text-slate-900">
|
||||||
<!-- <div class="absolute inset-0 bg-grid-pattern opacity-[0.4] -z-10"></div> -->
|
<section class="relative overflow-hidden border-b border-slate-100 bg-gradient-to-b from-slate-50 via-white to-white">
|
||||||
<div
|
<div class="pointer-events-none absolute inset-0">
|
||||||
class=":m: absolute top-0 right-0 -translate-y-1/2 translate-x-1/2 w-[800px] h-[800px] bg-primary-light/40 rounded-full blur-3xl -z-10 mix-blend-multiply animate-pulse duration-1000">
|
<div class="absolute inset-0 opacity-60 bg-[linear-gradient(rgba(148,163,184,0.12)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.12)_1px,transparent_1px)] bg-[length:64px_64px] [mask-image:linear-gradient(to_bottom,rgba(0,0,0,0.55),transparent_78%)]"></div>
|
||||||
</div>
|
<div class="absolute inset-x-0 top-0 h-[28rem] bg-[radial-gradient(circle_at_top,rgba(20,167,75,0.12),transparent_58%)]"></div>
|
||||||
<div
|
<div class="absolute -left-16 top-28 h-56 w-56 rounded-full bg-primary/10 blur-3xl"></div>
|
||||||
class=":m: absolute bottom-0 left-0 translate-y-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-teal-100/50 rounded-full blur-3xl -z-10 mix-blend-multiply">
|
<div class="absolute right-0 top-20 h-72 w-72 rounded-full bg-sky-100 blur-3xl"></div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="max-w-7xl m-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
||||||
<h1
|
|
||||||
class="text-5xl md:text-7xl font-extrabold tracking-tight text-slate-900 mb-6 leading-[1.1] animate-backwards">
|
|
||||||
Video infrastructure for <br>
|
|
||||||
<span class="text-gradient">modern internet.</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p class="text-xl text-slate-500 max-w-2xl mx-auto mb-10 leading-relaxed animate-backwards delay-50">
|
|
||||||
Seamlessly host, encode, and stream video with our developer-first API.
|
|
||||||
Optimized for speed, built for scale.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row justify-center gap-4">
|
|
||||||
<RouterLink to="/get-started" class="flex btn btn-success !rounded-xl !p-4 press-animated">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="46 -286 524 580">
|
|
||||||
<path d="M56 284v-560L560 4 56 284z" fill="#fff" />
|
|
||||||
</svg>
|
|
||||||
Get Started
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink to="/docs" class="flex btn btn-outline-primary !rounded-xl">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" viewBox="0 0 596 468">
|
|
||||||
<path
|
|
||||||
d="M10 314c0-63 41-117 98-136-1-8-2-16-2-24 0-79 65-144 144-144 55 0 104 31 128 77 14-8 30-13 48-13 53 0 96 43 96 96 0 16-4 31-10 44 44 20 74 64 74 116 0 71-57 128-128 128H154c-79 0-144-64-144-144zm199-73c-9 9-9 25 0 34s25 9 34 0l31-31v102c0 13 11 24 24 24s24-11 24-24V244l31 31c9 9 25 9 34 0s9-25 0-34l-72-72c-10-9-25-9-34 0l-72 72z"
|
|
||||||
fill="#14a74b" />
|
|
||||||
<path
|
|
||||||
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
|
|
||||||
fill="#fff" />
|
|
||||||
</svg>
|
|
||||||
Upload video
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="features" class="py-24 bg-white">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="mb-16 md:text-center max-w-3xl mx-auto">
|
|
||||||
<h2 class="text-3xl font-bold text-slate-900 mb-4">Everything you need to ship video</h2>
|
|
||||||
<p class="text-lg text-slate-500">Focus on building your product. We'll handle the complex video
|
|
||||||
infrastructure.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="relative mx-auto max-w-7xl px-4 pb-18 pt-28 sm:px-6 lg:px-8 lg:pb-24 lg:pt-34">
|
||||||
<div
|
<div class="grid items-center gap-12 lg:grid-cols-[1.02fr_0.98fr] lg:gap-14">
|
||||||
class=":m: md:col-span-2 bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-primary/60 transition-all group overflow-hidden relative">
|
<div>
|
||||||
<div class="relative z-10">
|
<div class="inline-flex items-center gap-2 rounded-full border border-primary/15 bg-white/90 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-primary shadow-sm">
|
||||||
<div
|
<span class="h-2 w-2 rounded-full bg-primary"></span>
|
||||||
class="w-12 h-12 bg-white rounded-xl flex items-center justify-center mb-6 border border-slate-100">
|
{{ t('home.features.live.onAir') }}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="532" viewBox="-8 -258 529 532">
|
|
||||||
<path
|
|
||||||
d="M342 32c-2 69-16 129-35 172-10 23-22 40-32 49-10 10-16 11-19 11h-1c-3 0-9-1-19-11-10-9-22-26-32-49-19-43-33-103-35-172h173zm169 0c-9 103-80 188-174 219 30-51 50-129 53-219h121zm-390 0c3 89 23 167 53 218C80 219 11 134 2 32h119zm53-266c-30 51-50 129-53 218H2c9-102 78-186 172-218zm82-14c3 0 9 1 19 11 10 9 22 26 32 50 19 42 33 102 35 171H169c3-69 16-129 35-171 10-24 22-41 32-50s16-11 19-11h1zm81 13c94 31 165 116 174 219H390c-3-90-23-168-53-219z"
|
|
||||||
fill="#059669" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold text-slate-900 mb-2">Global Edge Network</h3>
|
|
||||||
<p class="text-slate-500 max-w-md">Content delivered from 200+ PoPs worldwide. Automatic region
|
|
||||||
selection ensures the lowest latency for every viewer.</p>
|
|
||||||
</div>
|
|
||||||
<div class="absolute right-0 bottom-0 opacity-10 translate-x-1/4 translate-y-1/4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="-10 -258 532 532">
|
|
||||||
<path
|
|
||||||
d="M464 8c0-19-3-38-8-56l-27-5c-8-2-15 2-19 9-6 11-19 17-31 13l-14-5c-8-2-17 0-22 5-4 4-4 10 0 14l33 33c5 5 8 12 8 19 0 12-8 23-20 26l-6 1c-3 1-6 5-6 9v12c0 13-4 27-13 38l-25 34c-6 8-16 13-26 13-18 0-32-14-32-32V88c0-9-7-16-16-16h-32c-26 0-48-22-48-48V-4c0-13 6-24 16-32l39-30c6-4 13-6 20-6 3 0 7 1 10 2l32 10c7 3 15 3 22 1l36-9c10-2 17-11 17-22 0-8-5-16-13-20l-29-15c-3-2-8-1-11 2l-4 4c-4 4-11 7-17 7-4 0-8-1-11-3l-15-7c-7-4-15-2-20 4l-13 17c-6 7-16 8-22 1-3-2-5-6-5-10v-41c0-6-1-11-4-16l-10-18C102-154 48-79 48 8c0 115 93 208 208 208S464 123 464 8zM0 8c0-141 115-256 256-256S512-133 512 8 397 264 256 264 0 149 0 8z"
|
|
||||||
fill="#1e3050" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=":m: md:row-span-2 bg-slate-900 rounded-2xl p-8 text-white relative overflow-hidden group">
|
<h1 class="mt-7 max-w-4xl text-5xl font-bold leading-[1.02] tracking-tight text-slate-900 sm:text-6xl lg:text-7xl">
|
||||||
<div class=":m: absolute inset-0 bg-gradient-to-b from-slate-800/50 to-transparent"></div>
|
<span class="block">{{ t('home.hero.titleLine1') }}</span>
|
||||||
<div class="relative z-10">
|
<span class="mt-2 block bg-[linear-gradient(135deg,#0f172a_0%,#14a74b_55%,#0ea5e9_100%)] bg-clip-text text-transparent">
|
||||||
<div
|
{{ t('home.hero.titleLine2') }}
|
||||||
class=":m: w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mb-6 backdrop-blur-sm border border-white/10">
|
</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -146 468 384">
|
</h1>
|
||||||
<path
|
|
||||||
d="M392-136c-31 0-56 25-56 56v280c0 16 13 28 28 28h28c31 0 56-25 56-56V-80c0-31-25-56-56-56zM168 4c0-31 25-56 56-56h28c16 0 28 13 28 28v224c0 16-12 28-28 28h-56c-15 0-28-12-28-28V4zM0 88c0-31 25-56 56-56h28c16 0 28 13 28 28v140c0 16-12 28-28 28H56c-31 0-56-25-56-56V88z"
|
<p class="mt-6 max-w-2xl text-lg leading-8 text-slate-600 lg:text-xl">
|
||||||
fill="#fff" />
|
{{ t('home.hero.subtitle') }}
|
||||||
</svg>
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-9 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<RouterLink to="/sign-up" class="btn btn-success !rounded-xl !px-6 !py-3.5 shadow-sm">
|
||||||
|
{{ t('home.hero.getStarted') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink to="/login" class="btn btn-outline-primary !rounded-xl !px-6 !py-3.5">
|
||||||
|
{{ t('home.hero.uploadVideo') }}
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold mb-2">Live Streaming API</h3>
|
|
||||||
<p class="text-slate-400 text-sm leading-relaxed mb-8">Scale to millions of concurrent viewers
|
|
||||||
with ultra-low latency. RTMP ingest and HLS playback supported natively.</p>
|
|
||||||
|
|
||||||
<!-- Visual -->
|
<div class="mt-10 grid gap-4 sm:grid-cols-3">
|
||||||
<div
|
<div
|
||||||
class="bg-slate-800/50 rounded-lg p-4 border border-white/5 font-mono text-xs text-brand-300">
|
v-for="signal in signalItems"
|
||||||
<div class="flex justify-between items-center mb-3 border-b border-white/5 pb-2">
|
:key="signal.label"
|
||||||
<span class="text-slate-500">Live Status</span>
|
class="rounded-2xl border border-slate-200 bg-white px-5 py-4"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||||
|
{{ signal.label }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-xl font-bold text-slate-900">
|
||||||
|
{{ signal.value }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative mx-auto w-full max-w-[36rem] lg:mr-0">
|
||||||
|
<div class="rounded-[2rem] border border-slate-200 bg-white p-5 shadow-[0_24px_70px_rgba(15,23,42,0.08)] sm:p-6">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-100 pb-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||||
|
{{ t('home.features.live.status') }}
|
||||||
|
</p>
|
||||||
|
<h2 class="mt-2 text-2xl font-bold tracking-tight text-slate-900">
|
||||||
|
{{ t('home.features.live.title') }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary">
|
||||||
|
<span class="h-2 w-2 rounded-full bg-primary"></span>
|
||||||
|
{{ t('home.features.live.onAir') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-5 text-sm leading-7 text-slate-600 sm:text-base">
|
||||||
|
{{ t('home.features.live.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-5 rounded-[1.5rem] bg-slate-50 p-4 sm:p-5">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="signal in signalItems"
|
||||||
|
:key="signal.label"
|
||||||
|
class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white px-4 py-3"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-500">{{ signal.label }}</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-900">{{ signal.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 grid grid-cols-12 items-end gap-2">
|
||||||
|
<span
|
||||||
|
v-for="n in 12"
|
||||||
|
:key="n"
|
||||||
|
class="rounded-full bg-[linear-gradient(180deg,rgba(20,167,75,0.95),rgba(20,167,75,0.2))]"
|
||||||
|
:style="{ height: `${34 + (n % 5) * 12}px`, opacity: 0.4 + (n % 4) * 0.13 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-wrap gap-2.5">
|
||||||
<span
|
<span
|
||||||
class=":m: flex items-center gap-1.5 text-red-500 text-[10px] uppercase font-bold tracking-wider animate-pulse"><span
|
v-for="pill in featurePills"
|
||||||
class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span> On Air</span>
|
:key="pill"
|
||||||
|
class="rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-medium text-slate-600"
|
||||||
|
>
|
||||||
|
{{ pill }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
</div>
|
||||||
<div class="flex justify-between"><span class="text-slate-400">Bitrate:</span> <span
|
|
||||||
class="text-white">6000 kbps</span></div>
|
<div class="absolute -right-4 top-12 hidden w-56 rounded-2xl border border-slate-200 bg-white p-4 shadow-xl lg:block">
|
||||||
<div class="flex justify-between"><span class="text-slate-400">FPS:</span> <span
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||||
class="text-white">60</span></div>
|
{{ t('home.features.global.title') }}
|
||||||
<div class="flex justify-between"><span class="text-slate-400">Latency:</span> <span
|
</p>
|
||||||
class="text-brand-400">~2s</span></div>
|
<p class="mt-3 text-sm leading-6 text-slate-600">
|
||||||
|
{{ t('home.features.global.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute -left-4 bottom-8 hidden w-64 rounded-2xl border border-slate-200 bg-white p-4 shadow-xl lg:block">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-sm font-semibold text-slate-900">
|
||||||
|
{{ t('home.features.analytics.title') }}
|
||||||
|
</p>
|
||||||
|
<span class="rounded-full bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-500">
|
||||||
|
{{ t('home.features.live.onAir') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-slate-600">
|
||||||
|
{{ t('home.features.analytics.description') }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 grid grid-cols-4 items-end gap-2">
|
||||||
|
<span class="h-10 rounded-full bg-slate-200"></span>
|
||||||
|
<span class="h-18 rounded-full bg-primary/70"></span>
|
||||||
|
<span class="h-14 rounded-full bg-sky-300"></span>
|
||||||
|
<span class="h-22 rounded-full bg-slate-900"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Standard Feature -->
|
<section id="features" class="border-b border-slate-100 bg-slate-50/70 py-20">
|
||||||
<div
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
class=":m: bg-slate-50 rounded-2xl p-8 border border-slate-100 transition-all group hover:(border-brand-200 shadow-lg shadow-brand-500/5)">
|
<div class="mx-auto mb-14 max-w-3xl text-center">
|
||||||
<div
|
<p class="text-sm font-semibold uppercase tracking-[0.22em] text-primary">
|
||||||
class=":m: w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-purple-600 border border-slate-100">
|
{{ t('home.features.heading') }}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="0 0 570 570">
|
</p>
|
||||||
<path
|
<h2 class="mt-4 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
|
||||||
d="M50 428c-5 5-5 14 0 19s14 5 19 0l237-237c5-5 5-14 0-19s-14-5-19 0L50 428zm16-224c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zM174 60c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zm215 29c-5 5-5 14 0 19s14 5 19 0l39-39c5-5 5-14 0-19s-14-5-19 0l-39 39zm21 357c-5 5-5 14 0 19s14 5 19 0l18-18c5-5 5-14 0-19s-14-5-19 0l-18 18z"
|
{{ t('home.features.heading') }}
|
||||||
fill="#a6acb9" />
|
</h2>
|
||||||
<path
|
<p class="mt-4 text-lg leading-8 text-slate-600">
|
||||||
d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z"
|
{{ t('home.features.subtitle') }}
|
||||||
fill="#1e3050" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-slate-900 mb-2">Instant Encoding</h3>
|
|
||||||
<p class="text-slate-500 text-sm">Upload raw files and get optimized HLS/DASH streams in seconds.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Standard Feature -->
|
<div class="grid gap-6 lg:grid-cols-3">
|
||||||
<div
|
<article class="rounded-3xl border border-slate-200 bg-white p-8 transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-[0_18px_44px_rgba(15,23,42,0.08)]">
|
||||||
class=":m: bg-slate-50 rounded-2xl p-8 border border-slate-100 transition-all group hover:(border-brand-200 shadow-lg shadow-brand-500/5)">
|
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||||
<div
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="-8 -258 529 532" fill="none">
|
||||||
class=":m: w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-orange-600 border border-slate-100">
|
<path
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -226 532 468">
|
d="M342 32c-2 69-16 129-35 172-10 23-22 40-32 49-10 10-16 11-19 11h-1c-3 0-9-1-19-11-10-9-22-26-32-49-19-43-33-103-35-172h173zm169 0c-9 103-80 188-174 219 30-51 50-129 53-219h121zm-390 0c3 89 23 167 53 218C80 219 11 134 2 32h119zm53-266c-30 51-50 129-53 218H2c9-102 78-186 172-218zm82-14c3 0 9 1 19 11 10 9 22 26 32 50 19 42 33 102 35 171H169c3-69 16-129 35-171 10-24 22-41 32-50s16-11 19-11h1zm81 13c94 31 165 116 174 219H390c-3-90-23-168-53-219z"
|
||||||
<path
|
fill="currentColor"
|
||||||
d="M32-216c18 0 32 14 32 32v336c0 9 7 16 16 16h400c18 0 32 14 32 32s-14 32-32 32H80c-44 0-80-36-80-80v-336c0-18 14-32 32-32zM144-24c18 0 32 14 32 32v64c0 18-14 32-32 32s-32-14-32-32V8c0-18 14-32 32-32zm144-64V72c0 18-14 32-32 32s-32-14-32-32V-88c0-18 14-32 32-32s32 14 32 32zm80 32c18 0 32 14 32 32v96c0 18-14 32-32 32s-32-14-32-32v-96c0-18 14-32 32-32zm144-96V72c0 18-14 32-32 32s-32-14-32-32v-224c0-18 14-32 32-32s32 14 32 32z"
|
/>
|
||||||
fill="#1e3050" />
|
</svg>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
<h3 class="mt-5 text-2xl font-bold tracking-tight text-slate-900">
|
||||||
<h3 class="text-xl font-bold text-slate-900 mb-2">Deep Analytics</h3>
|
{{ t('home.features.global.title') }}
|
||||||
<p class="text-slate-500 text-sm">Session-level insights, quality of experience (QoE) metrics, and
|
</h3>
|
||||||
more.</p>
|
<p class="mt-3 text-base leading-7 text-slate-600">
|
||||||
</div>
|
{{ t('home.features.global.description') }}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</article>
|
||||||
</section>
|
|
||||||
<!-- Pricing -->
|
|
||||||
<section id="pricing" class="py-24 border-t border-slate-100 bg-white">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div class="text-center mb-16">
|
|
||||||
<h2 class="text-3xl font-bold text-slate-900 mb-4">{{ pricing.title }}</h2>
|
|
||||||
<p class="text-slate-500">{{ pricing.subtitle }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 w-full">
|
<article class="rounded-3xl border border-slate-200 bg-white p-8 transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-[0_18px_44px_rgba(15,23,42,0.08)]">
|
||||||
<div v-for="pack in pricing.packs" :key="pack.name"
|
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-violet-50 text-violet-600">
|
||||||
:class="cn(':uno: p-8 rounded-2xl relative overflow-hidden hover:border-primary transition-colors flex flex-col justify-between', pack.tag == 'POPULAR' ? 'border-primary/80 border-2' : 'border-slate-200 border')"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 570 570" fill="none">
|
||||||
:style="{ background: pack.bg }">
|
<path
|
||||||
<div v-if="pack.tag"
|
d="M50 428c-5 5-5 14 0 19s14 5 19 0l237-237c5-5 5-14 0-19s-14-5-19 0L50 428zm16-224c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zM174 60c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zm215 29c-5 5-5 14 0 19s14 5 19 0l39-39c5-5 5-14 0-19s-14-5-19 0l-39 39zm21 357c-5 5-5 14 0 19s14 5 19 0l18-18c5-5 5-14 0-19s-14-5-19 0l-18 18z"
|
||||||
class=":m: absolute top-0 right-0 bg-primary/80 text-white text-xs font-bold px-3 py-1 rounded-bl-lg uppercase">
|
fill="#a6acb9"
|
||||||
{{ pack.tag }}</div>
|
/>
|
||||||
<div>
|
<path
|
||||||
<h3 class="font-semibold text-slate-900 text-xl mb-2">{{ pack.name }}</h3>
|
d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z"
|
||||||
<div class="flex items-baseline gap-1 mb-6">
|
fill="currentColor"
|
||||||
<span class="text-4xl font-bold text-slate-900">{{ pack.price }}</span>
|
/>
|
||||||
<span class="text-slate-500">/mo</span>
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-5 text-2xl font-bold tracking-tight text-slate-900">
|
||||||
|
{{ t('home.features.encoding.title') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-3 text-base leading-7 text-slate-600">
|
||||||
|
{{ t('home.features.encoding.description') }}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="rounded-3xl border border-slate-200 bg-white p-8 transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-[0_18px_44px_rgba(15,23,42,0.08)]">
|
||||||
|
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-50 text-amber-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="-10 -226 532 468" fill="none">
|
||||||
|
<path
|
||||||
|
d="M32-216c18 0 32 14 32 32v336c0 9 7 16 16 16h400c18 0 32 14 32 32s-14 32-32 32H80c-44 0-80-36-80-80v-336c0-18 14-32 32-32zM144-24c18 0 32 14 32 32v64c0 18-14 32-32 32s-32-14-32-32V8c0-18 14-32 32-32zm144-64V72c0 18-14 32-32 32s-32-14-32-32V-88c0-18 14-32 32-32s32 14 32 32zm80 32c18 0 32 14 32 32v96c0 18-14 32-32 32s-32-14-32-32v-96c0-18 14-32 32-32zm144-96V72c0 18-14 32-32 32s-32-14-32-32v-224c0-18 14-32 32-32s32 14 32 32z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-5 text-2xl font-bold tracking-tight text-slate-900">
|
||||||
|
{{ t('home.features.analytics.title') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-3 text-base leading-7 text-slate-600">
|
||||||
|
{{ t('home.features.analytics.description') }}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 rounded-3xl border border-slate-200 bg-white p-6 sm:p-8">
|
||||||
|
<div class="grid gap-6 lg:grid-cols-[1fr_0.9fr] lg:items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold uppercase tracking-[0.22em] text-primary">
|
||||||
|
{{ t('home.features.live.title') }}
|
||||||
|
</p>
|
||||||
|
<h3 class="mt-3 text-3xl font-bold tracking-tight text-slate-900">
|
||||||
|
{{ t('home.features.live.title') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-4 max-w-2xl text-base leading-8 text-slate-600">
|
||||||
|
{{ t('home.features.live.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-[1.75rem] bg-slate-50 p-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="signal in signalItems"
|
||||||
|
:key="`summary-${signal.label}`"
|
||||||
|
class="flex items-center justify-between rounded-2xl border border-slate-200 bg-white px-4 py-3"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-slate-500">{{ signal.label }}</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-900">{{ signal.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="space-y-3 mb-8 text-sm text-slate-600">
|
|
||||||
<li v-for="value in pack.features" :key="value" class="flex items-center gap-3"><Check-Icon
|
|
||||||
class="fas fa-check text-brand-500" /> {{ value }}</li>
|
|
||||||
</ul>
|
|
||||||
<router-link to="/sign-up"
|
|
||||||
:class="cn('btn flex justify-center w-full !py-2.5', pack.tag == 'POPULAR' ? 'btn-primary' : 'btn-outline-primary')">{{
|
|
||||||
pack.buttonText }}</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
<section id="pricing" class="bg-white py-20">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="mx-auto mb-14 max-w-3xl text-center">
|
||||||
|
<p class="text-sm font-semibold uppercase tracking-[0.22em] text-primary">
|
||||||
|
{{ pricing.title }}
|
||||||
|
</p>
|
||||||
|
<h2 class="mt-4 text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl">
|
||||||
|
{{ pricing.title }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-4 text-lg leading-8 text-slate-600">
|
||||||
|
{{ pricing.subtitle }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-6 lg:grid-cols-3 lg:items-stretch">
|
||||||
|
<article
|
||||||
|
v-for="pack in pricing.packs"
|
||||||
|
:key="pack.name"
|
||||||
|
:class="[
|
||||||
|
'relative flex h-full flex-col overflow-hidden rounded-3xl border p-8 shadow-sm transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-[0_18px_44px_rgba(15,23,42,0.08)]',
|
||||||
|
isFeaturedPack(pack.tag)
|
||||||
|
? 'border-primary bg-primary/[0.04] ring-1 ring-primary/15'
|
||||||
|
: isScalePack(pack.tag)
|
||||||
|
? 'border-slate-200 bg-slate-50/80'
|
||||||
|
: 'border-slate-200 bg-white'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="pack.tag"
|
||||||
|
:class="[
|
||||||
|
'absolute right-5 top-5 rounded-full px-3 py-1 text-xs font-semibold',
|
||||||
|
isFeaturedPack(pack.tag) ? 'bg-primary text-white' : 'bg-slate-900 text-white'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ pack.tag }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||||
|
{{ pack.name }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 flex items-end gap-2">
|
||||||
|
<span class="text-5xl font-bold tracking-tight text-slate-900">{{ pack.price }}</span>
|
||||||
|
<span class="pb-2 text-slate-500">{{ t('home.pricing.perMonth') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="mt-8 space-y-4">
|
||||||
|
<li v-for="value in pack.features" :key="value" class="flex items-start gap-3">
|
||||||
|
<span class="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||||
|
<path d="M5 12.5l4.2 4.2L19 7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-600">{{ value }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
to="/sign-up"
|
||||||
|
:class="[
|
||||||
|
'mt-10 inline-flex w-full items-center justify-center !rounded-xl !py-3.5',
|
||||||
|
isFeaturedPack(pack.tag) ? 'btn btn-success' : 'btn btn-outline-primary'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ pack.buttonText }}
|
||||||
|
</RouterLink>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Head } from '@unhead/vue/components'
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
const pricing = {
|
|
||||||
title: "Simple, transparent pricing",
|
|
||||||
subtitle: "Choose the plan that fits your needs. No hidden fees.",
|
|
||||||
packs: [
|
|
||||||
{
|
|
||||||
name: "Hobby",
|
|
||||||
price: "$0",
|
|
||||||
features: [
|
|
||||||
"Unlimited upload",
|
|
||||||
"1 Hour of Storage",
|
|
||||||
"Standard Support",
|
|
||||||
],
|
|
||||||
buttonText: "Start Free",
|
|
||||||
tag: "",
|
|
||||||
bg: "#f9fafb",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pro",
|
|
||||||
price: "$29",
|
|
||||||
features: [
|
|
||||||
"Ads free player",
|
|
||||||
"Support M3U8",
|
|
||||||
"Unlimited upload",
|
|
||||||
"Custom ads"
|
|
||||||
],
|
|
||||||
buttonText: "Get Started",
|
|
||||||
tag: "POPULAR",
|
|
||||||
bg: "#eff6ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scale",
|
|
||||||
price: "$99",
|
|
||||||
features: [
|
|
||||||
"5 TB Bandwidth",
|
|
||||||
"500 Hours Storage",
|
|
||||||
"Priority Support"
|
|
||||||
],
|
|
||||||
buttonText: "Contact Sales",
|
|
||||||
tag: "Best Value",
|
|
||||||
bg: "#eef4f7",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -5,21 +5,21 @@
|
|||||||
<div class="flex items-center justify-between h-16">
|
<div class="flex items-center justify-between h-16">
|
||||||
<router-link to="/" class="flex items-center gap-2 cursor-pointer">
|
<router-link to="/" class="flex items-center gap-2 cursor-pointer">
|
||||||
<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />
|
<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />
|
||||||
<span class="font-bold text-xl tracking-tight text-slate-900">EcoStream</span>
|
<span class="font-bold text-xl tracking-tight text-slate-900">{{ t('app.name') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="#features"
|
<a href="#features"
|
||||||
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Features</a>
|
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">{{ t('home.nav.features') }}</a>
|
||||||
<a href="#pricing"
|
<a href="#pricing"
|
||||||
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Pricing</a>
|
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">{{ t('home.nav.pricing') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:flex items-center gap-4">
|
<div class="hidden md:flex items-center gap-4">
|
||||||
<RouterLink to="/login"
|
<RouterLink to="/login"
|
||||||
class="text-sm font-semibold text-slate-600 hover:text-slate-900 cursor-pointer">Log in
|
class="text-sm font-semibold text-slate-600 hover:text-slate-900 cursor-pointer">{{ t('home.nav.login') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink to="/sign-up"
|
<RouterLink to="/sign-up"
|
||||||
class="bg-slate-900 hover:bg-black text-white px-5 py-2.5 rounded-lg text-sm font-semibold cursor-pointer">
|
class="bg-slate-900 hover:bg-black text-white px-5 py-2.5 rounded-lg text-sm font-semibold cursor-pointer">
|
||||||
Start for free
|
{{ t('home.nav.startFree') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,47 +38,49 @@
|
|||||||
<div class="w-6 h-6 bg-brand-600 rounded flex items-center justify-center text-white">
|
<div class="w-6 h-6 bg-brand-600 rounded flex items-center justify-center text-white">
|
||||||
<img class="h-6 w-6" src="/apple-touch-icon.png" alt="Logo" />
|
<img class="h-6 w-6" src="/apple-touch-icon.png" alt="Logo" />
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-lg text-slate-900">EcoStream</span>
|
<span class="font-bold text-lg text-slate-900">{{ t('app.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-slate-500 text-sm max-w-xs">Building the video layer of the internet. Designed for
|
<p class="text-slate-500 text-sm max-w-xs">{{ t('home.footer.description') }}</p>
|
||||||
developers.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Product</h4>
|
<h4 class="font-semibold text-slate-900 mb-4 text-sm">{{ t('home.footer.product') }}</h4>
|
||||||
<ul class="space-y-2 text-sm text-slate-500">
|
<ul class="space-y-2 text-sm text-slate-500">
|
||||||
<li><a href="#" class="hover:text-brand-600">Features</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.productFeatures') }}</a></li>
|
||||||
<li><a href="#" class="hover:text-brand-600">Pricing</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.productPricing') }}</a></li>
|
||||||
<li><a href="#" class="hover:text-brand-600">Showcase</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.productShowcase') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Company</h4>
|
<h4 class="font-semibold text-slate-900 mb-4 text-sm">{{ t('home.footer.company') }}</h4>
|
||||||
<ul class="space-y-2 text-sm text-slate-500">
|
<ul class="space-y-2 text-sm text-slate-500">
|
||||||
<li><a href="#" class="hover:text-brand-600">About</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.companyAbout') }}</a></li>
|
||||||
<li><a href="#" class="hover:text-brand-600">Blog</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.companyBlog') }}</a></li>
|
||||||
<li><a href="#" class="hover:text-brand-600">Careers</a></li>
|
<li><a href="#" class="hover:text-brand-600">{{ t('home.footer.companyCareers') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Legal</h4>
|
<h4 class="font-semibold text-slate-900 mb-4 text-sm">{{ t('home.footer.legal') }}</h4>
|
||||||
<ul class="space-y-2 text-sm text-slate-500">
|
<ul class="space-y-2 text-sm text-slate-500">
|
||||||
<li><router-link to="/privacy" class="hover:text-brand-600">Privacy</router-link></li>
|
<li><router-link to="/privacy" class="hover:text-brand-600">{{ t('home.footer.privacy') }}</router-link></li>
|
||||||
<li><router-link to="/terms" class="hover:text-brand-600">Terms</router-link></li>
|
<li><router-link to="/terms" class="hover:text-brand-600">{{ t('home.footer.terms') }}</router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-8 border-t border-slate-100 text-center text-sm text-slate-400">
|
<div class="pt-8 border-t border-slate-100 text-center text-sm text-slate-400">
|
||||||
© 2026 EcoStream Inc. All rights reserved.
|
{{ t('home.footer.copyright', { year: 2026 }) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>EcoStream - Video infrastructure for modern internet</title>
|
<title>{{ t('home.head.title') }}</title>
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content="Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale." />
|
:content="t('home.head.description')" />
|
||||||
</Head>
|
</Head>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Head } from '@unhead/vue/components'
|
import { Head } from '@unhead/vue/components'
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,61 +1,101 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-4xl mx-auto space-y-10" style="opacity: 1; transform: none;">
|
<section class="relative overflow-hidden border-b border-slate-100 bg-gradient-to-b from-slate-50 via-white to-white">
|
||||||
<div class="grow pt-32 pb-12 px-4">
|
<div class="pointer-events-none absolute inset-0">
|
||||||
<div class="max-w-4xl mx-auto space-y-10">
|
<div class="absolute inset-0 opacity-55 bg-[linear-gradient(rgba(148,163,184,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.1)_1px,transparent_1px)] bg-[length:64px_64px] [mask-image:linear-gradient(to_bottom,rgba(0,0,0,0.45),transparent_78%)]"></div>
|
||||||
<div class="space-y-3">
|
<div class="absolute left-0 top-20 h-56 w-56 rounded-full bg-primary/10 blur-3xl"></div>
|
||||||
<p
|
<div class="absolute right-0 top-24 h-64 w-64 rounded-full bg-sky-100 blur-3xl"></div>
|
||||||
class="inline-block px-4 py-1.5 rounded-full bg-info/20 font-bold text-sm uppercase">
|
</div>
|
||||||
{{ pageContent.data.pageSubheading }}</p>
|
|
||||||
<h1 class="text-4xl md:text-5xl font-heading font-extrabold">{{ pageContent.data.pageHeading }}</h1>
|
<div class="relative mx-auto max-w-5xl px-4 pb-16 pt-28 sm:px-6 lg:px-8 lg:pb-20 lg:pt-34">
|
||||||
<p class="text-slate-600 text-lg font-medium">{{ pageContent.data.description }}</p>
|
<div class="mx-auto max-w-4xl">
|
||||||
|
<div class="inline-flex items-center rounded-full border border-primary/15 bg-white/90 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-primary shadow-sm">
|
||||||
|
{{ pageContent.data.pageSubheading }}
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-8 rounded-xl border border-gray-200 shadow-hard space-y-6">
|
|
||||||
<section v-for="(item, index) in pageContent.data.list" :key="index">
|
<div class="mt-6 max-w-3xl space-y-4">
|
||||||
<h2 class="text-2xl font-bold mb-4">{{ item.heading }}</h2>
|
<h1 class="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl lg:text-6xl">
|
||||||
<p class="leading-relaxed">{{ item.text }}</p>
|
{{ pageContent.data.pageHeading }}
|
||||||
</section>
|
</h1>
|
||||||
|
<p class="text-lg leading-8 text-slate-600">
|
||||||
|
{{ pageContent.data.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8 lg:p-10">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<section
|
||||||
|
v-for="(item, index) in pageContent.data.list"
|
||||||
|
:key="index"
|
||||||
|
class="rounded-2xl border border-slate-200 bg-slate-50/70 p-5 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-primary/25 hover:bg-white hover:shadow-[0_14px_32px_rgba(15,23,42,0.06)] sm:p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-sm font-bold text-primary">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="text-xl font-bold tracking-tight text-slate-900 sm:text-2xl">
|
||||||
|
{{ item.heading }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 leading-8 text-slate-600">
|
||||||
|
{{ item.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useHead} from "@unhead/vue";
|
import { computed } from 'vue';
|
||||||
const title = "Privacy Policy - Ecostream";
|
import { useTranslation } from 'i18next-vue';
|
||||||
const description = "Read about Ecostream's commitment to protecting your privacy and data security.";
|
import { useHead } from '@unhead/vue';
|
||||||
const pageContent = {
|
|
||||||
head: {
|
const { t } = useTranslation();
|
||||||
title,
|
|
||||||
meta: [
|
const pageContent = computed(() => {
|
||||||
{ name: "description", content: description },
|
const title = t('legal.privacy.title');
|
||||||
{ property: "og:title", content: title },
|
const description = t('legal.privacy.description');
|
||||||
{ property: "og:description", content: description },
|
|
||||||
{ property: "twitter:title", content: title },
|
return {
|
||||||
{ property: "twitter:description", content: description },
|
head: {
|
||||||
{ property: "twitter:image", content: "https://Ecostream.com/thumb.png" }
|
title,
|
||||||
]
|
meta: [
|
||||||
},
|
{ name: 'description', content: description },
|
||||||
data: {
|
{ property: 'og:title', content: title },
|
||||||
pageHeading: "Legal & Privacy Policy",
|
{ property: 'og:description', content: description },
|
||||||
pageSubheading: "Legal & Privacy Policy",
|
{ property: 'twitter:title', content: title },
|
||||||
description: "Our legal and privacy policy.",
|
{ property: 'twitter:description', content: description },
|
||||||
list: [{
|
{ property: 'twitter:image', content: 'https://Ecostream.com/thumb.png' }
|
||||||
heading: "1. Privacy Policy",
|
]
|
||||||
text: "At Ecostream, we take your privacy seriously. This policy describes how we collect, use, and protect your personal information. We only collect information that is necessary for the operation of our service, including email addresses for account creation and payment information for subscription processing."
|
|
||||||
},
|
},
|
||||||
{
|
data: {
|
||||||
heading: "2. Data Collection",
|
pageHeading: t('legal.privacy.pageHeading'),
|
||||||
text: "We collect data such as IP addresses, browser types, and access times to analyze trends and improve our service. Uploaded content is stored securely and is only accessed as required for the delivery of our hosting services."
|
pageSubheading: t('legal.privacy.pageSubheading'),
|
||||||
},
|
description: t('legal.privacy.pageDescription'),
|
||||||
{
|
list: [
|
||||||
heading: "3. Cookie Policy",
|
{
|
||||||
text: "We use cookies to maintain user sessions and preferences. By using our website, you consent to the use of cookies in accordance with this policy."
|
heading: t('legal.privacy.sections.policyTitle'),
|
||||||
},
|
text: t('legal.privacy.sections.policyText')
|
||||||
{
|
},
|
||||||
heading: "4. DMCA & Copyright",
|
{
|
||||||
text: "Ecostream respects the intellectual property rights of others. We respond to notices of alleged copyright infringement in accordance with the Digital Millennium Copyright Act (DMCA). Please report any copyright violations to our support team."
|
heading: t('legal.privacy.sections.dataCollectionTitle'),
|
||||||
}]
|
text: t('legal.privacy.sections.dataCollectionText')
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
useHead(pageContent.head);
|
heading: t('legal.privacy.sections.cookieTitle'),
|
||||||
</script>
|
text: t('legal.privacy.sections.cookieText')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: t('legal.privacy.sections.dmcaTitle'),
|
||||||
|
text: t('legal.privacy.sections.dmcaText')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead(() => pageContent.value.head);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,67 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-4xl mx-auto space-y-10" style="opacity: 1; transform: none;">
|
<section class="relative overflow-hidden border-b border-slate-100 bg-gradient-to-b from-slate-50 via-white to-white">
|
||||||
<div class="grow pt-32 pb-12 px-4">
|
<div class="pointer-events-none absolute inset-0">
|
||||||
<div class="max-w-4xl mx-auto space-y-10">
|
<div class="absolute inset-0 opacity-55 bg-[linear-gradient(rgba(148,163,184,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(148,163,184,0.1)_1px,transparent_1px)] bg-[length:64px_64px] [mask-image:linear-gradient(to_bottom,rgba(0,0,0,0.45),transparent_78%)]"></div>
|
||||||
<div class="space-y-3">
|
<div class="absolute left-0 top-20 h-56 w-56 rounded-full bg-primary/10 blur-3xl"></div>
|
||||||
<p
|
<div class="absolute right-0 top-24 h-64 w-64 rounded-full bg-sky-100 blur-3xl"></div>
|
||||||
class="inline-block px-4 py-1.5 rounded-full bg-info/20 font-bold text-sm uppercase">
|
</div>
|
||||||
{{ pageContent.data.pageSubheading }}</p>
|
|
||||||
<h1 class="text-4xl md:text-5xl font-heading font-extrabold">{{ pageContent.data.pageHeading }}</h1>
|
<div class="relative mx-auto max-w-5xl px-4 pb-16 pt-28 sm:px-6 lg:px-8 lg:pb-20 lg:pt-34">
|
||||||
<p class="text-slate-600 text-lg font-medium">{{ pageContent.data.description }}</p>
|
<div class="mx-auto max-w-4xl">
|
||||||
|
<div class="inline-flex items-center rounded-full border border-primary/15 bg-white/90 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-primary shadow-sm">
|
||||||
|
{{ pageContent.data.pageSubheading }}
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white p-8 rounded-xl border border-gray-200 shadow-hard space-y-6">
|
|
||||||
<section v-for="(item, index) in pageContent.data.list" :key="index">
|
<div class="mt-6 max-w-3xl space-y-4">
|
||||||
<h2 class="text-2xl font-bold mb-4">{{ item.heading }}</h2>
|
<h1 class="text-4xl font-bold tracking-tight text-slate-900 sm:text-5xl lg:text-6xl">
|
||||||
<p class="leading-relaxed">{{ item.text }}</p>
|
{{ pageContent.data.pageHeading }}
|
||||||
</section>
|
</h1>
|
||||||
|
<p class="text-lg leading-8 text-slate-600">
|
||||||
|
{{ pageContent.data.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm sm:p-8 lg:p-10">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<section
|
||||||
|
v-for="(item, index) in pageContent.data.list"
|
||||||
|
:key="index"
|
||||||
|
class="rounded-2xl border border-slate-200 bg-slate-50/70 p-5 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-primary/25 hover:bg-white hover:shadow-[0_14px_32px_rgba(15,23,42,0.06)] sm:p-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-sm font-bold text-primary">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="text-xl font-bold tracking-tight text-slate-900 sm:text-2xl">
|
||||||
|
{{ item.heading }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-3 leading-8 text-slate-600">
|
||||||
|
{{ item.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useHead} from "@unhead/vue";
|
import { computed } from 'vue';
|
||||||
const title = "Terms and Conditions - Ecostream";
|
import { useTranslation } from 'i18next-vue';
|
||||||
const description = "Read Ecostream's terms and conditions for using our video hosting and streaming services.";
|
import { useHead } from '@unhead/vue';
|
||||||
const pageContent = {
|
|
||||||
head: {
|
const { t } = useTranslation();
|
||||||
title,
|
|
||||||
meta: [
|
const pageContent = computed(() => {
|
||||||
{ name: "description", content: description },
|
const title = t('legal.terms.title');
|
||||||
{ property: "og:title", content: title },
|
const description = t('legal.terms.description');
|
||||||
{ property: "og:description", content: description },
|
|
||||||
{ property: "twitter:title", content: title },
|
return {
|
||||||
{ property: "twitter:description", content: description },
|
head: {
|
||||||
{ property: "twitter:image", content: "https://Ecostream.com/thumb.png" }
|
title,
|
||||||
]
|
meta: [
|
||||||
},
|
{ name: 'description', content: description },
|
||||||
data: {
|
{ property: 'og:title', content: title },
|
||||||
pageHeading: "Terms and Conditions Details",
|
{ property: 'og:description', content: description },
|
||||||
pageSubheading: "Terms and Conditions",
|
{ property: 'twitter:title', content: title },
|
||||||
description: "Our terms and conditions set forth important guidelines and rules for using Ecostream's services.",
|
{ property: 'twitter:description', content: description },
|
||||||
list: [
|
{ property: 'twitter:image', content: 'https://Ecostream.com/thumb.png' }
|
||||||
{
|
]
|
||||||
heading: "1. Acceptance of Terms",
|
},
|
||||||
text: "By accessing and using Ecostream, you accept and agree to be bound by the terms and provision of this agreement."
|
data: {
|
||||||
},
|
pageHeading: t('legal.terms.pageHeading'),
|
||||||
{
|
pageSubheading: t('legal.terms.pageSubheading'),
|
||||||
heading: "2. Service Usage",
|
description: t('legal.terms.pageDescription'),
|
||||||
text: "You agree to use our service only for lawful purposes. You are prohibited from posting or transmitting any unlawful, threatening, libelous, defamatory, obscene, or profane material. We reserve the right to terminate accounts that violate these terms."
|
list: [
|
||||||
},
|
{
|
||||||
{
|
heading: t('legal.terms.sections.acceptanceTitle'),
|
||||||
heading: "3. Content Ownership",
|
text: t('legal.terms.sections.acceptanceText')
|
||||||
text: "You retain all rights and ownership of the content you upload to Ecostream. However, by uploading content, you grant us a license to host, store, and display the content as necessary to provide our services."
|
},
|
||||||
},
|
{
|
||||||
{
|
heading: t('legal.terms.sections.usageTitle'),
|
||||||
heading: "4. Limitation of Liability",
|
text: t('legal.terms.sections.usageText')
|
||||||
text: "Ecostream shall not be liable for any direct, indirect, incidental, special, or consequential damages resulting from the use or inability to use our service."
|
},
|
||||||
},
|
{
|
||||||
{
|
heading: t('legal.terms.sections.ownershipTitle'),
|
||||||
heading: "5. Changes to Terms",
|
text: t('legal.terms.sections.ownershipText')
|
||||||
text: "We reserve the right to modify these terms at any time. Your continued use of the service after any such changes constitutes your acceptance of the new terms."
|
},
|
||||||
}
|
{
|
||||||
]
|
heading: t('legal.terms.sections.liabilityTitle'),
|
||||||
}
|
text: t('legal.terms.sections.liabilityText')
|
||||||
}
|
},
|
||||||
useHead(pageContent.head);
|
{
|
||||||
</script>
|
heading: t('legal.terms.sections.changesTitle'),
|
||||||
|
text: t('legal.terms.sections.changesText')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead(() => pageContent.value.head);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRouteLoading } from "@/composables/useRouteLoading";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { headSymbol, type ReactiveHead, type ResolvableValue } from "@unhead/vue";
|
import { headSymbol, type ReactiveHead, type ResolvableValue } from "@unhead/vue";
|
||||||
import { inject } from "vue";
|
import { inject } from "vue";
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
|
|
||||||
type RouteData = RouteRecordRaw & {
|
type RouteData = RouteRecordRaw & {
|
||||||
meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean };
|
meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean; requiresAdmin?: boolean };
|
||||||
children?: RouteData[];
|
children?: RouteData[];
|
||||||
};
|
};
|
||||||
const routes: RouteData[] = [
|
const routes: RouteData[] = [
|
||||||
@@ -63,11 +64,21 @@ const routes: RouteData[] = [
|
|||||||
name: "signup",
|
name: "signup",
|
||||||
component: () => import("./auth/signup.vue"),
|
component: () => import("./auth/signup.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "ref/:username",
|
||||||
|
name: "referral-entry",
|
||||||
|
beforeEnter: (to) => ({ name: "signup", query: { ref: String(to.params.username || "") } }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "forgot",
|
path: "forgot",
|
||||||
name: "forgot",
|
name: "forgot",
|
||||||
component: () => import("./auth/forgot.vue"),
|
component: () => import("./auth/forgot.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "auth/google/finalize",
|
||||||
|
name: "google-auth-finalize",
|
||||||
|
component: () => import("./auth/google-finalize.vue"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,16 +96,6 @@ const routes: RouteData[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: "upload",
|
|
||||||
// name: "upload",
|
|
||||||
// component: () => import("./upload/Upload.vue"),
|
|
||||||
// meta: {
|
|
||||||
// head: {
|
|
||||||
// title: "Upload - Holistream",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
path: "videos",
|
path: "videos",
|
||||||
children: [
|
children: [
|
||||||
@@ -114,16 +115,6 @@ const routes: RouteData[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: ":id",
|
|
||||||
// name: "video-detail",
|
|
||||||
// component: () => import("./video/DetailVideo.vue"),
|
|
||||||
// meta: {
|
|
||||||
// head: {
|
|
||||||
// title: "Edit Video - Holistream",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -156,7 +147,7 @@ const routes: RouteData[] = [
|
|||||||
{
|
{
|
||||||
path: "security",
|
path: "security",
|
||||||
name: "settings-security",
|
name: "settings-security",
|
||||||
component: () => import("./settings/pages/SecurityNConnected.vue"),
|
component: () => import("./settings/SecurityNConnected/SecurityNConnected.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Security & Connected Apps - Holistream",
|
title: "Security & Connected Apps - Holistream",
|
||||||
@@ -166,7 +157,7 @@ const routes: RouteData[] = [
|
|||||||
{
|
{
|
||||||
path: "billing",
|
path: "billing",
|
||||||
name: "settings-billing",
|
name: "settings-billing",
|
||||||
component: () => import("./settings/pages/Billing.vue"),
|
component: () => import("./settings/Billing/Billing.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Billing & Plans - Holistream",
|
title: "Billing & Plans - Holistream",
|
||||||
@@ -182,7 +173,7 @@ const routes: RouteData[] = [
|
|||||||
{
|
{
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
name: "settings-notifications",
|
name: "settings-notifications",
|
||||||
component: () => import("./settings/pages/NotificationSettings.vue"),
|
component: () => import("./settings/NotificationSettings/NotificationSettings.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Notifications - Holistream",
|
title: "Notifications - Holistream",
|
||||||
@@ -191,18 +182,12 @@ const routes: RouteData[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "player",
|
path: "player",
|
||||||
name: "settings-player",
|
redirect: { name: "settings-player-configs" },
|
||||||
component: () => import("./settings/pages/PlayerSettings.vue"),
|
|
||||||
meta: {
|
|
||||||
head: {
|
|
||||||
title: "Player Settings - Holistream",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "domains",
|
path: "domains",
|
||||||
name: "settings-domains",
|
name: "settings-domains",
|
||||||
component: () => import("./settings/pages/DomainsDns.vue"),
|
component: () => import("./settings/DomainsDns/DomainsDns.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Allowed Domains - Holistream",
|
title: "Allowed Domains - Holistream",
|
||||||
@@ -212,23 +197,49 @@ const routes: RouteData[] = [
|
|||||||
{
|
{
|
||||||
path: "ads",
|
path: "ads",
|
||||||
name: "settings-ads",
|
name: "settings-ads",
|
||||||
component: () => import("./settings/pages/AdsVast.vue"),
|
component: () => import("./settings/AdsVast/AdsVast.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Ads & VAST - Holistream",
|
title: "Ads & VAST - Holistream",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "player-configs",
|
||||||
|
name: "settings-player-configs",
|
||||||
|
component: () => import("./settings/PlayerConfigs/PlayerConfigs.vue"),
|
||||||
|
meta: {
|
||||||
|
head: {
|
||||||
|
title: "Player Configs - Holistream",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "danger",
|
path: "danger",
|
||||||
name: "settings-danger",
|
name: "settings-danger",
|
||||||
component: () => import("./settings/pages/DangerZone.vue"),
|
component: () => import("./settings/DangerZone/DangerZone.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
head: {
|
head: {
|
||||||
title: "Danger Zone - Holistream",
|
title: "Danger Zone - Holistream",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "admin",
|
||||||
|
meta: { requiresAdmin: true },
|
||||||
|
redirect: { name: "admin-overview" },
|
||||||
|
children: [
|
||||||
|
{ path: "users", name: "admin-users", component: () => import("./settings/admin/Users.vue") },
|
||||||
|
{ path: "videos", name: "admin-videos", component: () => import("./settings/admin/Videos.vue") },
|
||||||
|
{ path: "payments", name: "admin-payments", component: () => import("./settings/admin/Payments.vue") },
|
||||||
|
{ path: "plans", name: "admin-plans", component: () => import("./settings/admin/Plans.vue") },
|
||||||
|
{ path: "ad-templates", name: "admin-ad-templates", component: () => import("./settings/admin/AdTemplates.vue") },
|
||||||
|
{ path: "player-configs", name: "admin-player-configs", component: () => import("./settings/admin/PlayerConfigs.vue") },
|
||||||
|
{ path: "jobs", name: "admin-jobs", component: () => import("./settings/admin/Jobs.vue") },
|
||||||
|
{ path: "agents", name: "admin-agents", component: () => import("./settings/admin/Agents.vue") },
|
||||||
|
{ path: "logs", name: "admin-logs", component: () => import("./settings/admin/Logs.vue") },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -255,16 +266,38 @@ const createAppRouter = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loading = useRouteLoading()
|
||||||
router.beforeEach((to, from) => {
|
router.beforeEach((to, from) => {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const head = inject(headSymbol);
|
const head = inject(headSymbol);
|
||||||
(head as any).push(to.meta.head || {});
|
(head as any).push(to.meta.head || {});
|
||||||
|
if (to.fullPath !== from.fullPath && !import.meta.env.SSR) {
|
||||||
|
loading.start()
|
||||||
|
}
|
||||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||||
if (!auth.user) {
|
if (!auth.user) {
|
||||||
return { name: "login" };
|
return { name: "login" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.matched.some((record) => record.meta.requiresAdmin)) {
|
||||||
|
if (!auth.user) {
|
||||||
|
return { name: "login" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = String(auth.user.role || "").toLowerCase();
|
||||||
|
if (role !== "admin") {
|
||||||
|
return { name: "overview" };
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
router.afterEach(() => {
|
||||||
|
loading.finish()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.onError(() => {
|
||||||
|
loading.fail()
|
||||||
|
})
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,152 +1,85 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import NotificationActions from './components/NotificationActions.vue';
|
import NotificationActions from './components/NotificationActions.vue';
|
||||||
import NotificationList from './components/NotificationList.vue';
|
import NotificationList from './components/NotificationList.vue';
|
||||||
import NotificationTabs from './components/NotificationTabs.vue';
|
import NotificationTabs from './components/NotificationTabs.vue';
|
||||||
|
import { useNotifications } from '@/composables/useNotifications';
|
||||||
|
|
||||||
type NotificationType = 'info' | 'success' | 'warning' | 'error' | 'video' | 'payment' | 'system';
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
id: string;
|
|
||||||
type: NotificationType;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
time: string;
|
|
||||||
read: boolean;
|
|
||||||
actionUrl?: string;
|
|
||||||
actionLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const activeTab = ref('all');
|
const activeTab = ref('all');
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const notificationStore = useNotifications();
|
||||||
|
|
||||||
// Mock notifications data
|
onMounted(() => {
|
||||||
const notifications = ref<Notification[]>([
|
void notificationStore.fetchNotifications();
|
||||||
{
|
});
|
||||||
id: '1',
|
|
||||||
type: 'video',
|
const unreadCount = computed(() => notificationStore.unreadCount.value);
|
||||||
title: 'Video processing complete',
|
|
||||||
message: 'Your video "Summer Vacation 2024" has been successfully processed and is now ready to stream.',
|
|
||||||
time: '2 minutes ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/video',
|
|
||||||
actionLabel: 'View video'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'payment',
|
|
||||||
title: 'Payment successful',
|
|
||||||
message: 'Your subscription to Pro Plan has been renewed successfully. Next billing date: Feb 25, 2026.',
|
|
||||||
time: '1 hour ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/payments-and-plans',
|
|
||||||
actionLabel: 'View receipt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'warning',
|
|
||||||
title: 'Storage almost full',
|
|
||||||
message: 'You have used 85% of your storage quota. Consider upgrading your plan for more space.',
|
|
||||||
time: '3 hours ago',
|
|
||||||
read: false,
|
|
||||||
actionUrl: '/payments-and-plans',
|
|
||||||
actionLabel: 'Upgrade plan'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
type: 'success',
|
|
||||||
title: 'Upload successful',
|
|
||||||
message: 'Your video "Product Demo v2" has been uploaded successfully.',
|
|
||||||
time: '1 day ago',
|
|
||||||
read: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
type: 'system',
|
|
||||||
title: 'Scheduled maintenance',
|
|
||||||
message: 'We will perform scheduled maintenance on Jan 30, 2026 from 2:00 AM to 4:00 AM UTC.',
|
|
||||||
time: '2 days ago',
|
|
||||||
read: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
type: 'info',
|
|
||||||
title: 'New feature available',
|
|
||||||
message: 'We just launched video analytics! Track your video performance with detailed insights.',
|
|
||||||
time: '3 days ago',
|
|
||||||
read: true,
|
|
||||||
actionUrl: '/video',
|
|
||||||
actionLabel: 'Try it now'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => [
|
||||||
{ key: 'all', label: 'All', icon: 'i-lucide-inbox', count: notifications.value.length },
|
{ key: 'all', label: t('notification.tabs.all'), icon: 'i-lucide-inbox', count: notificationStore.notifications.value.length },
|
||||||
{ key: 'unread', label: 'Unread', icon: 'i-lucide-bell-dot', count: unreadCount.value },
|
{ key: 'unread', label: t('notification.tabs.unread'), icon: 'i-lucide-bell-dot', count: unreadCount.value },
|
||||||
{ key: 'video', label: 'Videos', icon: 'i-lucide-video', count: notifications.value.filter(n => n.type === 'video').length },
|
{ key: 'video', label: t('notification.tabs.videos'), icon: 'i-lucide-video', count: notificationStore.notifications.value.filter(n => n.type === 'video').length },
|
||||||
{ key: 'payment', label: 'Payments', icon: 'i-lucide-credit-card', count: notifications.value.filter(n => n.type === 'payment').length }
|
{ key: 'payment', label: t('notification.tabs.payments'), icon: 'i-lucide-credit-card', count: notificationStore.notifications.value.filter(n => n.type === 'payment').length },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filteredNotifications = computed(() => {
|
const filteredNotifications = computed(() => {
|
||||||
if (activeTab.value === 'all') return notifications.value;
|
if (activeTab.value === 'all') return notificationStore.notifications.value;
|
||||||
if (activeTab.value === 'unread') return notifications.value.filter(n => !n.read);
|
if (activeTab.value === 'unread') return notificationStore.notifications.value.filter(n => !n.read);
|
||||||
return notifications.value.filter(n => n.type === activeTab.value);
|
return notificationStore.notifications.value.filter(n => n.type === activeTab.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const unreadCount = computed(() => notifications.value.filter(n => !n.read).length);
|
const handleMarkRead = async (id: string) => {
|
||||||
|
await notificationStore.markRead(id);
|
||||||
const handleMarkRead = (id: string) => {
|
|
||||||
const notification = notifications.value.find(n => n.id === id);
|
|
||||||
if (notification) notification.read = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
notifications.value = notifications.value.filter(n => n.id !== id);
|
await notificationStore.deleteNotification(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = () => {
|
const handleMarkAllRead = async () => {
|
||||||
notifications.value.forEach(n => n.read = true);
|
await notificationStore.markAllRead();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = async () => {
|
||||||
notifications.value = [];
|
await notificationStore.clearAll();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Notifications"
|
:title="t('notification.title')"
|
||||||
description="Stay updated with your latest activities and alerts."
|
:description="t('notification.subtitle')"
|
||||||
:breadcrumbs="[
|
:breadcrumbs="[
|
||||||
{ label: 'Dashboard', to: '/' },
|
{ label: t('pageHeader.dashboard'), to: '/' },
|
||||||
{ label: 'Notifications' }
|
{ label: t('nav.notification') }
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<div class="w-full max-w-4xl mx-auto mt-6">
|
<div class="w-full max-w-4xl mx-auto mt-6">
|
||||||
<div class="notification-container bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
|
<div class="notification-container bg-white rounded-2xl border border-gray-200 p-6 shadow-sm">
|
||||||
<NotificationActions
|
<NotificationActions
|
||||||
:loading="loading"
|
:loading="notificationStore.loading.value"
|
||||||
:total-count="notifications.length"
|
:total-count="notificationStore.notifications.value.length"
|
||||||
:unread-count="unreadCount"
|
:unread-count="unreadCount"
|
||||||
@mark-all-read="handleMarkAllRead"
|
@mark-all-read="handleMarkAllRead"
|
||||||
@clear-all="handleClearAll"
|
@clear-all="handleClearAll"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NotificationTabs
|
<NotificationTabs
|
||||||
:tabs="tabs"
|
:tabs="tabs"
|
||||||
:active-tab="activeTab"
|
:active-tab="activeTab"
|
||||||
@update:active-tab="activeTab = $event"
|
@update:active-tab="activeTab = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NotificationList
|
<NotificationList
|
||||||
:notifications="filteredNotifications"
|
:notifications="filteredNotifications"
|
||||||
:loading="loading"
|
:loading="notificationStore.loading.value"
|
||||||
@mark-read="handleMarkRead"
|
@mark-read="handleMarkRead"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
@@ -10,6 +12,8 @@ const emit = defineEmits<{
|
|||||||
markAllRead: [];
|
markAllRead: [];
|
||||||
clearAll: [];
|
clearAll: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,35 +22,35 @@ const emit = defineEmits<{
|
|||||||
<div class="stats flex items-center gap-4">
|
<div class="stats flex items-center gap-4">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<span class="i-lucide-bell w-4 h-4 text-gray-400"></span>
|
<span class="i-lucide-bell w-4 h-4 text-gray-400"></span>
|
||||||
<span class="text-gray-600">{{ totalCount }} notifications</span>
|
<span class="text-gray-600">{{ t('notification.stats.total', { count: totalCount }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="unreadCount > 0" class="flex items-center gap-2 text-sm">
|
<div v-if="unreadCount > 0" class="flex items-center gap-2 text-sm">
|
||||||
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
|
||||||
<span class="text-primary font-medium">{{ unreadCount }} unread</span>
|
<span class="text-primary font-medium">{{ t('notification.stats.unread', { count: unreadCount }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions flex items-center gap-2">
|
<div class="actions flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="unreadCount > 0"
|
v-if="unreadCount > 0"
|
||||||
@click="emit('markAllRead')"
|
@click="emit('markAllRead')"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-primary
|
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-primary
|
||||||
hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="i-lucide-check-check w-4 h-4"></span>
|
<span class="i-lucide-check-check w-4 h-4"></span>
|
||||||
Mark all read
|
{{ t('notification.actions.markAllRead') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="totalCount > 0"
|
v-if="totalCount > 0"
|
||||||
@click="emit('clearAll')"
|
@click="emit('clearAll')"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-red-600
|
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-red-600
|
||||||
hover:bg-red-50 rounded-lg transition-colors flex items-center gap-2"
|
hover:bg-red-50 rounded-lg transition-colors flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span class="i-lucide-trash w-4 h-4"></span>
|
<span class="i-lucide-trash w-4 h-4"></span>
|
||||||
Clear all
|
{{ t('notification.actions.clearAll') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import InfoIcon from '@/components/icons/InfoIcon.vue';
|
import InfoIcon from '@/components/icons/InfoIcon.vue';
|
||||||
import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue';
|
import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue';
|
||||||
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
|
||||||
@@ -31,6 +32,8 @@ const emit = defineEmits<{
|
|||||||
delete: [id: string];
|
delete: [id: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const iconComponent = computed(() => {
|
const iconComponent = computed(() => {
|
||||||
const icons: Record<string, any> = {
|
const icons: Record<string, any> = {
|
||||||
info: InfoIcon,
|
info: InfoIcon,
|
||||||
@@ -70,12 +73,10 @@ const bgClass = computed(() => {
|
|||||||
'flex items-start gap-4 group cursor-pointer relative',
|
'flex items-start gap-4 group cursor-pointer relative',
|
||||||
bgClass
|
bgClass
|
||||||
]" @click="emit('markRead', notification.id)">
|
]" @click="emit('markRead', notification.id)">
|
||||||
<!-- Icon -->
|
|
||||||
<div v-if="!isDrawer" class="flex-shrink-0 w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
|
<div v-if="!isDrawer" class="flex-shrink-0 w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
<component :is="iconComponent" :class="[iconColorClass, 'w-5 h-5']" />
|
<component :is="iconComponent" :class="[iconColorClass, 'w-5 h-5']" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<h4 :class="['font-semibold text-gray-900', !notification.read && 'text-primary-700']">
|
<h4 :class="['font-semibold text-gray-900', !notification.read && 'text-primary-700']">
|
||||||
@@ -85,30 +86,27 @@ const bgClass = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-600 mt-1 line-clamp-2">{{ notification.message }}</p>
|
<p class="text-sm text-gray-600 mt-1 line-clamp-2">{{ notification.message }}</p>
|
||||||
|
|
||||||
<!-- Action Button -->
|
|
||||||
<router-link v-if="notification.actionUrl" :to="notification.actionUrl"
|
<router-link v-if="notification.actionUrl" :to="notification.actionUrl"
|
||||||
class="inline-flex items-center gap-1 text-sm text-primary font-medium mt-2 hover:underline">
|
class="inline-flex items-center gap-1 text-sm text-primary font-medium mt-2 hover:underline">
|
||||||
{{ notification.actionLabel || 'View Details' }}
|
{{ notification.actionLabel || t('notification.item.viewDetails') }}
|
||||||
<ArrowRightIcon class="w-4 h-4" />
|
<ArrowRightIcon class="w-4 h-4" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div v-if="!isDrawer"
|
<div v-if="!isDrawer"
|
||||||
class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1">
|
class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1">
|
||||||
<button v-if="!notification.read" @click.stop="emit('markRead', notification.id)"
|
<button v-if="!notification.read" @click.stop="emit('markRead', notification.id)"
|
||||||
class="p-2 rounded-lg hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
class="p-2 rounded-lg hover:bg-gray-200 text-gray-500 hover:text-gray-700 transition-colors"
|
||||||
title="Mark as read">
|
:title="t('notification.item.markAsRead')">
|
||||||
<CheckMarkIcon class="w-4 h-4" />
|
<CheckMarkIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button @click.stop="emit('delete', notification.id)"
|
<button @click.stop="emit('delete', notification.id)"
|
||||||
class="p-2 rounded-lg hover:bg-red-100 text-gray-500 hover:text-red-600 transition-colors"
|
class="p-2 rounded-lg hover:bg-red-100 text-gray-500 hover:text-red-600 transition-colors"
|
||||||
title="Delete">
|
:title="t('notification.item.delete')">
|
||||||
<TrashIcon class="w-4 h-4" />
|
<TrashIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unread indicator -->
|
|
||||||
<div v-if="!notification.read"
|
<div v-if="!notification.read"
|
||||||
class="absolute left-2 top-1/10 -translate-y-1/2 w-2 h-2 rounded-full bg-primary">
|
class="absolute left-2 top-1/10 -translate-y-1/2 w-2 h-2 rounded-full bg-primary">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import NotificationItem from './NotificationItem.vue';
|
import NotificationItem from './NotificationItem.vue';
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -22,15 +23,16 @@ const emit = defineEmits<{
|
|||||||
markRead: [id: string];
|
markRead: [id: string];
|
||||||
delete: [id: string];
|
delete: [id: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="notification-list space-y-3">
|
<div class="notification-list space-y-3">
|
||||||
<!-- Loading skeleton -->
|
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<div
|
<div
|
||||||
v-for="i in 5"
|
v-for="i in 5"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="p-4 rounded-xl border border-gray-200 animate-pulse"
|
class="p-4 rounded-xl border border-gray-200 animate-pulse"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
@@ -43,7 +45,6 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Notification items -->
|
|
||||||
<template v-else-if="notifications.length > 0">
|
<template v-else-if="notifications.length > 0">
|
||||||
<NotificationItem
|
<NotificationItem
|
||||||
v-for="notification in notifications"
|
v-for="notification in notifications"
|
||||||
@@ -54,16 +55,15 @@ const emit = defineEmits<{
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<div
|
||||||
<div
|
v-else
|
||||||
v-else
|
|
||||||
class="py-16 text-center"
|
class="py-16 text-center"
|
||||||
>
|
>
|
||||||
<div class="w-20 h-20 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
|
<div class="w-20 h-20 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
<span class="i-lucide-bell-off w-10 h-10 text-gray-400"></span>
|
<span class="i-lucide-bell-off w-10 h-10 text-gray-400"></span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-1">No notifications</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-1">{{ t('notification.empty.title') }}</h3>
|
||||||
<p class="text-gray-500">You're all caught up! Check back later.</p>
|
<p class="text-gray-500">{{ t('notification.empty.subtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,76 +1,74 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { client, type ModelVideo } from '@/api/client';
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
|
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||||
|
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import NameGradient from './components/NameGradient.vue';
|
import NameGradient from './components/NameGradient.vue';
|
||||||
import QuickActions from './components/QuickActions.vue';
|
import QuickActions from './components/QuickActions.vue';
|
||||||
import RecentVideos from './components/RecentVideos.vue';
|
import RecentVideos from './components/RecentVideos.vue';
|
||||||
import StatsOverview from './components/StatsOverview.vue';
|
import StatsOverview from './components/StatsOverview.vue';
|
||||||
|
import type { StatProps } from '@/components/dashboard/StatsCard.vue';
|
||||||
const loading = ref(true);
|
import { formatBytes, isAdmin } from '@/lib/utils';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
const AdminOverview = defineAsyncComponent(() => import('./components/AdminOverview.vue'));
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const recentVideosLoading = ref(true);
|
||||||
const recentVideos = ref<ModelVideo[]>([]);
|
const recentVideos = ref<ModelVideo[]>([]);
|
||||||
|
const { data: usageSnapshot, isPending: isUsagePending, refresh } = useUsageQuery();
|
||||||
|
|
||||||
// Mock stats data (in real app, fetch from API)
|
const stats = computed<StatProps[]>(() => [
|
||||||
const stats = ref({
|
{
|
||||||
totalVideos: 0,
|
title: 'overview.stats.totalVideos',
|
||||||
totalViews: 0,
|
value: usageSnapshot.value?.totalVideos ?? 0,
|
||||||
storageUsed: 0,
|
trend: { value: 12, isPositive: true }
|
||||||
storageLimit: 10737418240, // 10GB in bytes
|
},
|
||||||
uploadsThisMonth: 0
|
{
|
||||||
});
|
title: 'overview.stats.totalViews',
|
||||||
|
value: recentVideos.value.reduce((sum, v: any) => sum + (v.views || 0), 0),
|
||||||
|
trend: { value: 8, isPositive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'overview.stats.storageUsed',
|
||||||
|
value: `${formatBytes(usageSnapshot.value?.totalStorage ?? 0)} / ${t('overview.stats.unlimited')}`,
|
||||||
|
color: 'warning',
|
||||||
|
trend: { value: 5, isPositive: false }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const statsLoading = computed(() => recentVideosLoading.value || (isUsagePending.value && !usageSnapshot.value));
|
||||||
|
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
loading.value = true;
|
recentVideosLoading.value = true;
|
||||||
try {
|
try {
|
||||||
// Fetch recent videos
|
const response = await rpcClient.listVideos({ page: 1, limit: 5 });
|
||||||
const response = await client.videos.videosList({ page: 1, limit: 5 });
|
recentVideos.value = response.videos ?? [];
|
||||||
const body = response.data as any;
|
|
||||||
|
|
||||||
if (body.data && Array.isArray(body.data)) {
|
|
||||||
recentVideos.value = body.data;
|
|
||||||
stats.value.totalVideos = body.data.length;
|
|
||||||
} else if (Array.isArray(body)) {
|
|
||||||
recentVideos.value = body;
|
|
||||||
stats.value.totalVideos = body.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate mock stats
|
|
||||||
stats.value.totalViews = recentVideos.value.reduce((sum, v: any) => sum + (v.views || 0), 0);
|
|
||||||
stats.value.storageUsed = recentVideos.value.reduce((sum, v) => sum + (v.size || 0), 0);
|
|
||||||
stats.value.uploadsThisMonth = recentVideos.value.filter(v => {
|
|
||||||
const uploadDate = new Date(v.created_at || '');
|
|
||||||
const now = new Date();
|
|
||||||
return uploadDate.getMonth() === now.getMonth() && uploadDate.getFullYear() === now.getFullYear();
|
|
||||||
}).length;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch dashboard data:', err);
|
console.error('Failed to fetch dashboard data:', err);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
recentVideosLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
refresh();
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard-overview">
|
<div class="dashboard-overview">
|
||||||
<PageHeader :title="NameGradient" description="Welcome back, Here's what's happening with your videos." :breadcrumbs="[
|
<PageHeader :title="NameGradient" :description="$t('overview.welcome.subtitle')" :breadcrumbs="[
|
||||||
{ label: 'Dashboard' }
|
{ label: $t('pageHeader.dashboard') }
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
<AdminOverview v-if="isAdmin(auth.user?.role)" />
|
||||||
<StatsOverview :loading="loading" :stats="stats" />
|
<template v-else>
|
||||||
|
<StatsOverview :loading="statsLoading" :stats="stats" />
|
||||||
<!-- Quick Actions -->
|
<QuickActions :loading="recentVideosLoading" />
|
||||||
<QuickActions :loading="loading" />
|
<RecentVideos :loading="recentVideosLoading" :videos="recentVideos" />
|
||||||
|
</template>
|
||||||
<!-- Recent Videos -->
|
|
||||||
<RecentVideos :loading="loading" :videos="recentVideos" />
|
|
||||||
|
|
||||||
<!-- Storage Usage -->
|
|
||||||
<!-- <StorageUsage :loading="loading" :stats="stats" /> -->
|
<!-- <StorageUsage :loading="loading" :stats="stats" /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
72
src/routes/overview/components/AdminOverview.vue
Normal file
72
src/routes/overview/components/AdminOverview.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { client as rpcClient } from "@/api/rpcclient";
|
||||||
|
import { useQuery } from "@pinia/colada";
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import StatsOverview from "./StatsOverview.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
// const dashboard = ref<AdminDashboard | null>(null);
|
||||||
|
|
||||||
|
const cards = computed(() => {
|
||||||
|
const data = dashboard.value;
|
||||||
|
return [
|
||||||
|
{ title: "Total users", value: data?.totalUsers ?? 0, note: `${data?.newUsersToday ?? 0} new today`, tone: 'accent' as const },
|
||||||
|
{ title: "Total videos", value: data?.totalVideos ?? 0, note: `${data?.newVideosToday ?? 0} new today`, tone: 'success' as const },
|
||||||
|
{ title: "Payments", value: data?.totalPayments ?? 0, note: "Completed finance events", tone: 'warning' as const },
|
||||||
|
{ title: "Revenue", value: data?.totalRevenue ?? 0, note: "Lifetime gross amount", tone: 'neutral' as const },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondaryCards = computed(() => {
|
||||||
|
const data = dashboard.value;
|
||||||
|
return [
|
||||||
|
{ title: "Active subscriptions", value: data?.activeSubscriptions ?? 0 },
|
||||||
|
{ title: "Ad templates", value: data?.totalAdTemplates ?? 0 },
|
||||||
|
{ title: "New users today", value: data?.newUsersToday ?? 0 },
|
||||||
|
{ title: "New videos today", value: data?.newVideosToday ?? 0 },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const highlights = computed(() => {
|
||||||
|
const data = dashboard.value;
|
||||||
|
return [
|
||||||
|
{ label: "Acquisition", value: `${data?.newUsersToday ?? 0} user signups in the current day window.` },
|
||||||
|
{ label: "Content velocity", value: `${data?.newVideosToday ?? 0} newly created videos landed today.` },
|
||||||
|
{ label: "Catalog depth", value: `${data?.totalAdTemplates ?? 0} ad templates available to pair with uploads.` },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: dashboard, isLoading, refresh } = useQuery({
|
||||||
|
key: () => ['admin-dashboard'],
|
||||||
|
query: () => rpcClient.getAdminDashboard(),
|
||||||
|
});
|
||||||
|
onMounted(refresh);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<StatsOverview :loading="isLoading" :stats="cards" />
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div v-for="card in secondaryCards" :key="card.title"
|
||||||
|
class="rounded-lg border border-border bg-muted/15 px-4 py-4">
|
||||||
|
<div class="text-[11px] font-medium text-foreground/55">{{ card.title }}</div>
|
||||||
|
<div class="mt-3 text-2xl font-semibold tracking-tight text-foreground">{{ isLoading ? '—' : card.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-border bg-muted/15 p-4">
|
||||||
|
<div class="text-[11px] font-medium text-foreground/55">Operations notes</div>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
<div v-for="item in highlights" :key="item.label"
|
||||||
|
class="rounded-2xl border border-border bg-background px-4 py-3">
|
||||||
|
<div class="text-[11px] font-medium text-foreground/55">{{ item.label }}</div>
|
||||||
|
<div class="mt-1 text-sm leading-6 text-foreground/70">{{ item.value }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-3xl font-bold text-gray-900 mb-1">
|
<div class="text-3xl font-bold text-gray-900 mb-1">
|
||||||
<span class=":uno: bg-[linear-gradient(130deg,#14a74b_0%,#22c55e_35%,#10b981_65%,#06b6d4_100%)] bg-clip-text text-transparent">Hello, {{ auth.user?.username }}</span>
|
<span class=":uno: bg-[linear-gradient(130deg,#14a74b_0%,#22c55e_35%,#10b981_65%,#06b6d4_100%)] bg-clip-text text-transparent">{{ $t('overview.welcome.title', { name: auth.user?.username || $t('app.name') }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import Credit from '@/components/icons/Credit.vue';
|
|||||||
import Upload from '@/components/icons/Upload.vue';
|
import Upload from '@/components/icons/Upload.vue';
|
||||||
import Video from '@/components/icons/Video.vue';
|
import Video from '@/components/icons/Video.vue';
|
||||||
import { useUIState } from '@/stores/uiState';
|
import { useUIState } from '@/stores/uiState';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import Referral from './Referral.vue';
|
import Referral from './Referral.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
@@ -14,33 +17,34 @@ defineProps<Props>();
|
|||||||
|
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const quickActions = [
|
const quickActions = computed(() => [
|
||||||
{
|
{
|
||||||
title: 'Upload Video',
|
title: t('overview.quickActions.uploadVideo.title'),
|
||||||
description: 'Upload a new video to your library',
|
description: t('overview.quickActions.uploadVideo.description'),
|
||||||
icon: Upload,
|
icon: Upload,
|
||||||
onClick: () => uiState.toggleUploadDialog()
|
onClick: () => uiState.toggleUploadDialog()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Video Library',
|
title: t('overview.quickActions.videoLibrary.title'),
|
||||||
description: 'Browse all your videos',
|
description: t('overview.quickActions.videoLibrary.description'),
|
||||||
icon: Video,
|
icon: Video,
|
||||||
onClick: () => router.push('/video')
|
onClick: () => router.push('/videos')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Analytics',
|
title: t('overview.quickActions.analytics.title'),
|
||||||
description: 'Track performance & insights',
|
description: t('overview.quickActions.analytics.description'),
|
||||||
icon: Chart,
|
icon: Chart,
|
||||||
onClick: () => { }
|
onClick: () => { }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Manage Plan',
|
title: t('overview.quickActions.managePlan.title'),
|
||||||
description: 'Upgrade or change your plan',
|
description: t('overview.quickActions.managePlan.description'),
|
||||||
icon: Credit,
|
icon: Credit,
|
||||||
onClick: () => router.push('/payments-and-plans')
|
onClick: () => router.push('/settings/billing')
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -63,16 +67,16 @@ const quickActions = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="mb-8">
|
<div v-else class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4">Quick Actions</h2>
|
<h2 class="text-xl font-semibold mb-4">{{ t('overview.quickActions.title') }}</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[
|
<button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[
|
||||||
'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-surface',
|
'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-header',
|
||||||
'border border-gray-300 hover:border-primary hover:shadow-lg',
|
'border border-gray-300 hover:border-primary hover:shadow-lg',
|
||||||
'group press-animated',
|
'group press-animated',
|
||||||
]">
|
]">
|
||||||
<div
|
<div
|
||||||
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted group-hover:bg-primary/10">
|
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted-dark group-hover:bg-primary/10">
|
||||||
<component filled :is="action.icon" class="w-6 h-6" />
|
<component filled :is="action.icon" class="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>
|
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>
|
||||||
|
|||||||
@@ -1,133 +1,163 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ModelVideo } from '@/api/client';
|
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||||
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
||||||
import { formatBytes, formatDate, formatDuration } from '@/lib/utils';
|
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||||
|
import { formatDate, formatDuration } from '@/lib/utils';
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useUIState } from '@/stores/uiState';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
videos: ModelVideo[];
|
videos: ModelVideo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const uiState = useUIState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const getStatusClass = (status?: string) => {
|
const getStatusClass = (status?: string) => {
|
||||||
switch (status?.toLowerCase()) {
|
switch (status?.toLowerCase()) {
|
||||||
case 'ready': return 'bg-green-100 text-green-700';
|
case 'ready': return 'bg-green-100 text-green-700';
|
||||||
case 'processing': return 'bg-yellow-100 text-yellow-700';
|
case 'processing': return 'bg-yellow-100 text-yellow-700';
|
||||||
case 'failed': return 'bg-red-100 text-red-700';
|
case 'failed': return 'bg-red-100 text-red-700';
|
||||||
default: return 'bg-gray-100 text-gray-700';
|
default: return 'bg-gray-100 text-gray-700';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columns = computed<ColumnDef<ModelVideo>[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'video',
|
||||||
|
header: t('overview.recentVideos.table.video'),
|
||||||
|
cell: ({ row }) => h('div', { class: 'flex items-center gap-3' }, [
|
||||||
|
h('div', { class: 'h-12 w-20 flex-shrink-0 overflow-hidden rounded bg-gray-200' }, row.original.thumbnail
|
||||||
|
? h('img', {
|
||||||
|
src: row.original.thumbnail,
|
||||||
|
alt: row.original.title,
|
||||||
|
class: 'h-full w-full object-cover',
|
||||||
|
})
|
||||||
|
: h('div', { class: 'flex h-full w-full items-center justify-center' }, [
|
||||||
|
h('span', { class: 'i-heroicons-film text-xl text-gray-400' }),
|
||||||
|
])),
|
||||||
|
h('div', { class: 'min-w-0 flex-1' }, [
|
||||||
|
h('p', { class: 'truncate font-medium text-gray-900' }, row.original.title),
|
||||||
|
h('p', { class: 'truncate text-sm text-gray-500' }, row.original.description || t('overview.recentVideos.noDescription')),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-xs font-medium uppercase tracking-wider text-gray-500',
|
||||||
|
cellClass: 'px-6 py-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: t('overview.recentVideos.table.status'),
|
||||||
|
accessorFn: row => row.status || '',
|
||||||
|
cell: ({ row }) => h('span', {
|
||||||
|
class: ['whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium', getStatusClass(row.original.status)],
|
||||||
|
}, row.original.status || t('overview.recentVideos.unknownStatus')),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-xs font-medium uppercase tracking-wider text-gray-500',
|
||||||
|
cellClass: 'px-6 py-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duration',
|
||||||
|
header: t('overview.recentVideos.table.duration'),
|
||||||
|
accessorFn: row => Number(row.duration || 0),
|
||||||
|
cell: ({ row }) => h('span', { class: 'text-sm text-gray-500' }, formatDuration(row.original.duration)),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-xs font-medium uppercase tracking-wider text-gray-500',
|
||||||
|
cellClass: 'px-6 py-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'createdAt',
|
||||||
|
header: t('overview.recentVideos.table.uploadDate'),
|
||||||
|
accessorFn: row => row.createdAt || '',
|
||||||
|
cell: ({ row }) => h('span', { class: 'text-sm text-gray-500' }, formatDate(row.original.createdAt)),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-xs font-medium uppercase tracking-wider text-gray-500',
|
||||||
|
cellClass: 'px-6 py-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: t('overview.recentVideos.table.actions'),
|
||||||
|
enableSorting: false,
|
||||||
|
cell: () => h('div', { class: 'flex items-center gap-2' }, [
|
||||||
|
h('button', {
|
||||||
|
class: 'rounded p-1.5 transition-colors hover:bg-gray-100',
|
||||||
|
title: t('overview.recentVideos.actionEdit'),
|
||||||
|
}, [h('span', { class: 'i-heroicons-pencil h-4 w-4 text-gray-600' })]),
|
||||||
|
h('button', {
|
||||||
|
class: 'rounded p-1.5 transition-colors hover:bg-gray-100',
|
||||||
|
title: t('overview.recentVideos.actionShare'),
|
||||||
|
}, [h('span', { class: 'i-heroicons-share h-4 w-4 text-gray-600' })]),
|
||||||
|
h('button', {
|
||||||
|
class: 'rounded p-1.5 transition-colors hover:bg-red-100',
|
||||||
|
title: t('overview.recentVideos.actionDelete'),
|
||||||
|
}, [h('span', { class: 'i-heroicons-trash h-4 w-4 text-red-600' })]),
|
||||||
|
]),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-xs font-medium uppercase tracking-wider text-gray-500',
|
||||||
|
cellClass: 'px-6 py-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div class="w-32 h-6 bg-gray-200 rounded animate-pulse" />
|
<div class="h-6 w-32 rounded bg-gray-200 animate-pulse" />
|
||||||
<div class="w-20 h-4 bg-gray-200 rounded animate-pulse" />
|
<div class="h-4 w-20 rounded bg-gray-200 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white">
|
||||||
<div class="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
|
<div v-for="i in 5" :key="i" class="border-b border-gray-200 p-4 last:border-b-0">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="w-16 h-10 bg-gray-200 rounded animate-pulse" />
|
<div class="h-10 w-16 rounded bg-gray-200 animate-pulse" />
|
||||||
<div class="flex-1 space-y-2">
|
<div class="flex-1 space-y-2">
|
||||||
<div class="w-[30%] h-4 bg-gray-200 rounded animate-pulse" />
|
<div class="h-4 w-[30%] rounded bg-gray-200 animate-pulse" />
|
||||||
<div class="w-[20%] h-3 bg-gray-200 rounded animate-pulse" />
|
<div class="h-3 w-[20%] rounded bg-gray-200 animate-pulse" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h2 class="text-xl font-semibold">Recent Videos</h2>
|
|
||||||
<router-link to="/video"
|
|
||||||
class="text-sm text-primary hover:underline font-medium flex items-center gap-1">
|
|
||||||
View all
|
|
||||||
<span class="i-heroicons-arrow-right w-4 h-4" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EmptyState v-if="videos.length === 0" title="No videos found"
|
|
||||||
description="You haven't uploaded any videos yet. Start by uploading your first video!"
|
|
||||||
imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png" actionLabel="Upload Video"
|
|
||||||
:onAction="() => router.push('/upload')" />
|
|
||||||
|
|
||||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead class="bg-gray-50 border-b border-gray-200">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Video</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Duration</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Upload Date</th>
|
|
||||||
<th
|
|
||||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-gray-200">
|
|
||||||
<tr v-for="video in videos" :key="video.id" class="hover:bg-gray-50 transition-colors">
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0">
|
|
||||||
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title"
|
|
||||||
class="w-full h-full object-cover" />
|
|
||||||
<div v-else class="w-full h-full flex items-center justify-center">
|
|
||||||
<span class="i-heroicons-film text-gray-400 text-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<p class="font-medium text-gray-900 truncate">{{ video.title }}</p>
|
|
||||||
<p class="text-sm text-gray-500 truncate">
|
|
||||||
{{ video.description || 'No description' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<span
|
|
||||||
:class="['px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap', getStatusClass(video.status)]">
|
|
||||||
{{ video.status || 'Unknown' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">
|
|
||||||
{{ formatDuration(video.duration) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">
|
|
||||||
{{ formatDate(video.created_at) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
|
|
||||||
<span class="i-heroicons-pencil w-4 h-4 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Share">
|
|
||||||
<span class="i-heroicons-share w-4 h-4 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
<button class="p-1.5 hover:bg-red-100 rounded transition-colors" title="Delete">
|
|
||||||
<span class="i-heroicons-trash w-4 h-4 text-red-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<h2 class="text-xl font-semibold">{{ t('overview.recentVideos.title') }}</h2>
|
||||||
|
<router-link to="/videos" class="flex items-center gap-1 text-sm font-medium text-primary hover:underline">
|
||||||
|
{{ t('overview.recentVideos.viewAll') }}
|
||||||
|
<span class="i-heroicons-arrow-right h-4 w-4" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmptyState
|
||||||
|
v-if="videos.length === 0"
|
||||||
|
:title="t('overview.recentVideos.emptyTitle')"
|
||||||
|
:description="t('overview.recentVideos.emptyDescription')"
|
||||||
|
imageUrl="https://cdn-icons-png.flaticon.com/512/7486/7486747.png"
|
||||||
|
:actionLabel="t('overview.recentVideos.emptyAction')"
|
||||||
|
:onAction="() => uiState.toggleUploadDialog()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BaseTable
|
||||||
|
v-else
|
||||||
|
:data="props.videos"
|
||||||
|
:columns="columns"
|
||||||
|
:get-row-id="(row, index) => row.id || `recent-video-${index}`"
|
||||||
|
wrapperClass="rounded-xl border border-gray-200 bg-white"
|
||||||
|
tableClass="w-full"
|
||||||
|
headerRowClass="bg-gray-50 border-b border-gray-200"
|
||||||
|
bodyRowClass="hover:bg-gray-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-surface">
|
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-header">
|
||||||
<div class="flex flex-col space-y-1.5 p-6">
|
<div class="flex flex-col space-y-1.5 p-6">
|
||||||
<h3 class="text-lg font-semibold leading-none tracking-tight">Referral Link</h3>
|
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ t('overview.referral.title') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 pt-0 space-y-4">
|
<div class="p-6 pt-0 space-y-4">
|
||||||
<p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from
|
<p class="text-sm text-gray-600 font-medium">{{ t('overview.referral.subtitle') }}</p>
|
||||||
referred users!</p>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<AppInput class="w-full" readonly type="text" :modelValue="url" @click="copyToClipboard" />
|
<AppInput class="w-full" readonly type="text" :modelValue="url" @click="copyToClipboard" />
|
||||||
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied">
|
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied" :aria-label="t('common.copy')">
|
||||||
<svg v-if="!isCopied" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
<svg v-if="!isCopied" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true">
|
stroke-linejoin="round" class="lucide lucide-copy" aria-hidden="true">
|
||||||
@@ -28,19 +27,30 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { ref } from 'vue';
|
import { useTranslation } from 'i18next-vue';
|
||||||
const auth = useAuthStore()
|
import { computed, ref } from 'vue';
|
||||||
const isCopied = ref(false)
|
|
||||||
const url = location.origin + '/ref/' + auth.user?.username
|
const auth = useAuthStore();
|
||||||
const copyToClipboard = ($event: MouseEvent) => {
|
const isCopied = ref(false);
|
||||||
// ($event.target as HTMLInputElement)?.select
|
const { t } = useTranslation();
|
||||||
if ($event.target instanceof HTMLInputElement) {
|
|
||||||
$event.target.select()
|
const url = computed(() => {
|
||||||
|
if (typeof location === 'undefined') {
|
||||||
|
return auth.user?.username ? `/ref/${auth.user.username}` : '';
|
||||||
}
|
}
|
||||||
navigator.clipboard.writeText(url)
|
return `${location.origin}/ref/${auth.user?.username || ''}`;
|
||||||
isCopied.value = true
|
});
|
||||||
|
|
||||||
|
const copyToClipboard = ($event: MouseEvent) => {
|
||||||
|
if ($event.target instanceof HTMLInputElement) {
|
||||||
|
$event.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(url.value);
|
||||||
|
isCopied.value = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isCopied.value = false
|
isCopied.value = false;
|
||||||
}, 3000)
|
}, 3000);
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import StatsCard from '@/components/dashboard/StatsCard.vue';
|
import StatsCard, { type StatProps } from '@/components/dashboard/StatsCard.vue';
|
||||||
import { formatBytes } from '@/lib/utils';
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
stats: {
|
stats: StatProps[]
|
||||||
totalVideos: number;
|
|
||||||
totalViews: number;
|
|
||||||
storageUsed: number;
|
|
||||||
storageLimit: number;
|
|
||||||
uploadsThisMonth: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const { t, i18next } = useTranslation();
|
||||||
|
const localeTag = computed(() => i18next.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<div v-for="i in 4" :key="i" class="bg-surface rounded-xl border border-gray-200 p-6">
|
<div v-for="i in stats.length" :key="i" class="bg-header rounded-xl border border-gray-200 p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="w-20 h-4 bg-gray-200 rounded animate-pulse mb-2" />
|
<div class="w-20 h-4 bg-gray-200 rounded animate-pulse mb-2" />
|
||||||
@@ -29,16 +26,7 @@ defineProps<Props>();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<StatsCard title="Total Videos" :value="stats.totalVideos" :trend="{ value: 12, isPositive: true }" />
|
<StatsCard v-for="stat in stats" :key="stat.title" v-bind="stat"/>
|
||||||
|
|
||||||
<StatsCard title="Total Views" :value="stats.totalViews.toLocaleString()"
|
|
||||||
:trend="{ value: 8, isPositive: true }" />
|
|
||||||
|
|
||||||
<StatsCard title="Storage Used"
|
|
||||||
:value="`${formatBytes(stats.storageUsed)} / ${formatBytes(stats.storageLimit)}`" color="warning" />
|
|
||||||
|
|
||||||
<StatsCard title="Uploads This Month" :value="stats.uploadsThisMonth" color="success"
|
|
||||||
:trend="{ value: 25, isPositive: true }" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatBytes } from '@/lib/utils';
|
import { formatBytes } from '@/lib/utils';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -12,6 +13,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const storagePercentage = computed(() => {
|
const storagePercentage = computed(() => {
|
||||||
return Math.round((props.stats.storageUsed / props.stats.storageLimit) * 100);
|
return Math.round((props.stats.storageUsed / props.stats.storageLimit) * 100);
|
||||||
@@ -24,21 +26,21 @@ const storageBreakdown = computed(() => {
|
|||||||
const total = videoSize + thumbSize + otherSize;
|
const total = videoSize + thumbSize + otherSize;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: 'Videos', size: videoSize, percentage: (videoSize / (total || 1)) * 100, color: 'bg-primary' },
|
{ label: t('overview.storage.breakdown.videos'), size: videoSize, percentage: (videoSize / (total || 1)) * 100, color: 'bg-primary' },
|
||||||
{ label: 'Thumbnails & Assets', size: thumbSize, percentage: (thumbSize / (total || 1)) * 100, color: 'bg-blue-500' },
|
{ label: t('overview.storage.breakdown.thumbnails'), size: thumbSize, percentage: (thumbSize / (total || 1)) * 100, color: 'bg-blue-500' },
|
||||||
{ label: 'Other Files', size: otherSize, percentage: (otherSize / (total || 1)) * 100, color: 'bg-gray-400' },
|
{ label: t('overview.storage.breakdown.other'), size: otherSize, percentage: (otherSize / (total || 1)) * 100, color: 'bg-gray-400' },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!loading" class="bg-white rounded-xl border border-gray-200 p-6">
|
<div v-if="!loading" class="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
<h2 class="text-xl font-semibold mb-4">Storage Usage</h2>
|
<h2 class="text-xl font-semibold mb-4">{{ t('overview.storage.title') }}</h2>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span class="text-sm font-medium text-gray-700">
|
<span class="text-sm font-medium text-gray-700">
|
||||||
{{ formatBytes(stats.storageUsed) }} of {{ formatBytes(stats.storageLimit) }} used
|
{{ t('overview.storage.usedOfLimit', { used: formatBytes(stats.storageUsed), limit: formatBytes(stats.storageLimit) }) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-medium" :class="storagePercentage > 80 ? 'text-danger' : 'text-gray-700'">
|
<span class="text-sm font-medium" :class="storagePercentage > 80 ? 'text-danger' : 'text-gray-700'">
|
||||||
{{ storagePercentage }}%
|
{{ storagePercentage }}%
|
||||||
@@ -66,10 +68,10 @@ const storageBreakdown = computed(() => {
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<span class="i-heroicons-exclamation-triangle w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
<span class="i-heroicons-exclamation-triangle w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-yellow-800">Storage running low</p>
|
<p class="text-sm font-medium text-yellow-800">{{ t('overview.storage.lowStorage.title') }}</p>
|
||||||
<p class="text-sm text-yellow-700 mt-1">
|
<p class="text-sm text-yellow-700 mt-1">
|
||||||
Consider upgrading your plan to get more storage.
|
{{ t('overview.storage.lowStorage.message') }}
|
||||||
<router-link to="/plans" class="underline font-medium">View plans</router-link>
|
<router-link to="/plans" class="underline font-medium">{{ t('overview.storage.lowStorage.viewPlans') }}</router-link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore();
|
||||||
|
const { t } = useTranslation();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-gradient-to-r to-success/20 p-4 sm:p-6 md:p-8 rounded-xl border-2 border-success/30 mb-8">
|
<div class="bg-gradient-to-r to-success/20 p-4 sm:p-6 md:p-8 rounded-xl border-2 border-success/30 mb-8">
|
||||||
<h1 class="text-2xl sm:text-3xl md:text-4xl font-extrabold text-foreground mb-2">Welcome back, {{
|
<h1 class="text-2xl sm:text-3xl md:text-4xl font-extrabold text-foreground mb-2">
|
||||||
auth.user?.username }}! 👋
|
{{ t('overview.welcome.title', { name: auth.user?.username || t('app.name') }) }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm sm:text-base text-gray-600 font-medium">Here's what's happening with your content
|
<p class="text-sm sm:text-base text-gray-600 font-medium">{{ t('overview.welcome.subtitle') }}</p>
|
||||||
today.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
348
src/routes/settings/AdsVast/AdsVast.vue
Normal file
348
src/routes/settings/AdsVast/AdsVast.vue
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { client as rpcClient } from '@/api/rpcclient';
|
||||||
|
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
|
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useQuery } from '@pinia/colada';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import AdsVastDialog from './components/AdsVastDialog.vue';
|
||||||
|
import AdsVastNotices from './components/AdsVastNotices.vue';
|
||||||
|
import AdsVastTable from './components/AdsVastTable.tsx';
|
||||||
|
import AdsVastToolbar from './components/AdsVastToolbar.vue';
|
||||||
|
import type {
|
||||||
|
AdTemplate,
|
||||||
|
CreateAdTemplateRequest
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const toast = useAppToast();
|
||||||
|
const confirm = useAppConfirm();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const createInitialFormData = (): CreateAdTemplateRequest => ({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
vastTagUrl: '',
|
||||||
|
adFormat: 'pre-roll',
|
||||||
|
duration: undefined,
|
||||||
|
isActive: true,
|
||||||
|
isDefault: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showAddDialog = ref(false);
|
||||||
|
const editingTemplate = ref<AdTemplate | null>(null);
|
||||||
|
const saving = ref(false);
|
||||||
|
const deletingId = ref<string | null>(null);
|
||||||
|
const togglingId = ref<string | null>(null);
|
||||||
|
const defaultingId = ref<string | null>(null);
|
||||||
|
const formData = ref<CreateAdTemplateRequest>(createInitialFormData());
|
||||||
|
|
||||||
|
const isFreePlan = computed(() => !auth.user?.plan_id);
|
||||||
|
const isMutating = computed(() => saving.value || deletingId.value !== null || togglingId.value !== null || defaultingId.value !== null);
|
||||||
|
|
||||||
|
const { data: templatesSnapshot, error, isPending, refetch } = useQuery({
|
||||||
|
key: () => ['settings', 'ad-templates'],
|
||||||
|
query: async () => {
|
||||||
|
const response = await rpcClient.listAdTemplates();
|
||||||
|
return response.templates || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = computed<AdTemplate[]>(() => templatesSnapshot.value || []);
|
||||||
|
const isInitialLoading = computed(() => isPending.value && !templatesSnapshot.value);
|
||||||
|
const canCreateTemplate = computed(() => !isFreePlan.value && !isInitialLoading.value && !isMutating.value);
|
||||||
|
const canEditDialog = computed(() => !isFreePlan.value && !saving.value);
|
||||||
|
|
||||||
|
const getErrorMessage = (value: any, fallback: string) => value?.error?.message || value?.message || value?.data?.message || fallback;
|
||||||
|
|
||||||
|
const showActionErrorToast = (value: any) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.adsVast.toast.failedSummary'),
|
||||||
|
detail: getErrorMessage(value, t('settings.adsVast.toast.failedDetail')),
|
||||||
|
life: 5000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUpgradeRequiredToast = () => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('settings.adsVast.toast.upgradeRequiredSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.upgradeRequiredDetail'),
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensurePaidPlan = () => {
|
||||||
|
if (!isFreePlan.value) return true;
|
||||||
|
showUpgradeRequiredToast();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(error, (value, previous) => {
|
||||||
|
if (!value || value === previous || isMutating.value) return;
|
||||||
|
showActionErrorToast(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = createInitialFormData();
|
||||||
|
editingTemplate.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showAddDialog.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
if (!ensurePaidPlan()) return;
|
||||||
|
resetForm();
|
||||||
|
showAddDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyTemplateToForm = (template: AdTemplate) => {
|
||||||
|
formData.value = {
|
||||||
|
name: template.name || '',
|
||||||
|
description: template.description || '',
|
||||||
|
vastTagUrl: template.vastTagUrl || '',
|
||||||
|
adFormat: template.adFormat || 'pre-roll',
|
||||||
|
duration: template.duration,
|
||||||
|
isActive: template.isActive,
|
||||||
|
isDefault: Boolean(template.isDefault),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (template: AdTemplate) => {
|
||||||
|
if (!ensurePaidPlan()) return;
|
||||||
|
applyTemplateToForm(template);
|
||||||
|
editingTemplate.value = template;
|
||||||
|
showAddDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRequestBody = (enabled = true): Parameters<typeof rpcClient.createAdTemplate>[0] => ({
|
||||||
|
...formData.value,
|
||||||
|
name: (formData.value.name || '').trim(),
|
||||||
|
description: '',
|
||||||
|
vastTagUrl: (formData.value.vastTagUrl || '').trim(),
|
||||||
|
adFormat: formData.value.adFormat || 'pre-roll',
|
||||||
|
duration: formData.value.adFormat === 'mid-roll' ? formData.value.duration : undefined,
|
||||||
|
isActive: enabled,
|
||||||
|
isDefault: enabled ? Boolean(formData.value.isDefault) : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (saving.value || !ensurePaidPlan()) return;
|
||||||
|
|
||||||
|
if (!(formData.value.name || '').trim()) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.adsVast.toast.nameRequiredSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.nameRequiredDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(formData.value.vastTagUrl || '').trim()) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.adsVast.toast.urlRequiredSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.urlRequiredDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new URL(formData.value.vastTagUrl || '');
|
||||||
|
} catch {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.adsVast.toast.invalidUrlSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.invalidUrlDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formData.value.adFormat === 'mid-roll' && (!formData.value.duration || formData.value.duration <= 0)) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('settings.adsVast.toast.durationRequiredSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.durationRequiredDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
if (editingTemplate.value) {
|
||||||
|
await rpcClient.updateAdTemplate({
|
||||||
|
id: editingTemplate.value.id || '',
|
||||||
|
...buildRequestBody(Boolean(editingTemplate.value.isActive)),
|
||||||
|
});
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('settings.adsVast.toast.updatedSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.updatedDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await rpcClient.createAdTemplate(buildRequestBody(true));
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('settings.adsVast.toast.createdSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.createdDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
closeDialog();
|
||||||
|
} catch (value: any) {
|
||||||
|
console.error(value);
|
||||||
|
showActionErrorToast(value);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = async (template: AdTemplate, nextValue: boolean) => {
|
||||||
|
if (!ensurePaidPlan()) return;
|
||||||
|
|
||||||
|
togglingId.value = template.id || null;
|
||||||
|
try {
|
||||||
|
await rpcClient.updateAdTemplate({
|
||||||
|
id: template.id || '',
|
||||||
|
name: template.name || '',
|
||||||
|
description: template.description || '',
|
||||||
|
vastTagUrl: template.vastTagUrl || '',
|
||||||
|
adFormat: template.adFormat,
|
||||||
|
duration: template.adFormat === 'mid-roll' ? template.duration : undefined,
|
||||||
|
isActive: nextValue,
|
||||||
|
isDefault: nextValue ? Boolean(template.isDefault) : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: nextValue
|
||||||
|
? t('settings.adsVast.toast.enabledSummary')
|
||||||
|
: t('settings.adsVast.toast.disabledSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.toggleDetail', {
|
||||||
|
name: template.name || '',
|
||||||
|
state: nextValue
|
||||||
|
? t('settings.adsVast.state.enabled')
|
||||||
|
: t('settings.adsVast.state.disabled'),
|
||||||
|
}),
|
||||||
|
life: 2000,
|
||||||
|
});
|
||||||
|
} catch (value: any) {
|
||||||
|
console.error(value);
|
||||||
|
showActionErrorToast(value);
|
||||||
|
} finally {
|
||||||
|
togglingId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetDefault = async (template: AdTemplate) => {
|
||||||
|
if (Boolean(template.isDefault) || !Boolean(template.isActive) || !ensurePaidPlan()) return;
|
||||||
|
|
||||||
|
defaultingId.value = template.id || null;
|
||||||
|
try {
|
||||||
|
await rpcClient.updateAdTemplate({
|
||||||
|
id: template.id || '',
|
||||||
|
name: template.name || '',
|
||||||
|
description: template.description || '',
|
||||||
|
vastTagUrl: template.vastTagUrl || '',
|
||||||
|
adFormat: template.adFormat,
|
||||||
|
duration: template.adFormat === 'mid-roll' ? template.duration : undefined,
|
||||||
|
isActive: template.isActive,
|
||||||
|
isDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('settings.adsVast.toast.defaultUpdatedSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.defaultUpdatedDetail', { name: template.name || '' }),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} catch (value: any) {
|
||||||
|
console.error(value);
|
||||||
|
showActionErrorToast(value);
|
||||||
|
} finally {
|
||||||
|
defaultingId.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (template: AdTemplate) => {
|
||||||
|
if (!ensurePaidPlan()) return;
|
||||||
|
|
||||||
|
confirm.require({
|
||||||
|
message: t('settings.adsVast.confirm.deleteMessage', { name: template.name || '' }),
|
||||||
|
header: t('settings.adsVast.confirm.deleteHeader'),
|
||||||
|
acceptLabel: t('settings.adsVast.confirm.deleteAccept'),
|
||||||
|
rejectLabel: t('settings.adsVast.confirm.deleteReject'),
|
||||||
|
accept: async () => {
|
||||||
|
deletingId.value = template.id || null;
|
||||||
|
try {
|
||||||
|
await rpcClient.deleteAdTemplate({ id: template.id || '' });
|
||||||
|
await refetch();
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: t('settings.adsVast.toast.deletedSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.deletedDetail'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} catch (value: any) {
|
||||||
|
console.error(value);
|
||||||
|
showActionErrorToast(value);
|
||||||
|
} finally {
|
||||||
|
deletingId.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsSectionCard
|
||||||
|
:title="t('settings.content.ads.title')"
|
||||||
|
:description="t('settings.content.ads.subtitle')"
|
||||||
|
bodyClass=""
|
||||||
|
>
|
||||||
|
<template #header-actions>
|
||||||
|
<AdsVastToolbar :disabled="!canCreateTemplate" @create="openAddDialog" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<AdsVastNotices :is-free-plan="isFreePlan" />
|
||||||
|
|
||||||
|
<AdsVastTable
|
||||||
|
:templates="templates"
|
||||||
|
:is-initial-loading="isInitialLoading"
|
||||||
|
:is-read-only="isFreePlan"
|
||||||
|
:is-mutating="isMutating"
|
||||||
|
:saving="saving"
|
||||||
|
:deleting-id="deletingId"
|
||||||
|
:toggling-id="togglingId"
|
||||||
|
:defaulting-id="defaultingId"
|
||||||
|
@edit="openEditDialog"
|
||||||
|
@delete="handleDelete"
|
||||||
|
@toggle-active="handleToggle($event.template, $event.value)"
|
||||||
|
@set-default="handleSetDefault"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AdsVastDialog
|
||||||
|
:visible="showAddDialog"
|
||||||
|
:editing-template="editingTemplate"
|
||||||
|
:form-data="formData"
|
||||||
|
:saving="saving"
|
||||||
|
:can-edit="canEditDialog"
|
||||||
|
@update:visible="showAddDialog = $event"
|
||||||
|
@update:form-data="formData = $event"
|
||||||
|
@save="handleSave"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
</SettingsSectionCard>
|
||||||
|
</template>
|
||||||
194
src/routes/settings/AdsVast/components/AdsVastDialog.vue
Normal file
194
src/routes/settings/AdsVast/components/AdsVastDialog.vue
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
|
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||||
|
import AppInput from '@/components/ui/AppInput.vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { AdTemplate, CreateAdTemplateRequest } from '../types';
|
||||||
|
|
||||||
|
const AD_FORMAT_OPTIONS = ['pre-roll', 'mid-roll', 'post-roll'] as const;
|
||||||
|
|
||||||
|
type AdFormatOption = NonNullable<CreateAdTemplateRequest['adFormat']>;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
editingTemplate: AdTemplate | null;
|
||||||
|
formData: CreateAdTemplateRequest;
|
||||||
|
saving: boolean;
|
||||||
|
canEdit: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'update:formData', value: CreateAdTemplateRequest): void;
|
||||||
|
(e: 'save'): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const title = computed(() => props.editingTemplate
|
||||||
|
? t('settings.adsVast.dialog.editTitle')
|
||||||
|
: t('settings.adsVast.dialog.createTitle'));
|
||||||
|
|
||||||
|
const canToggleDefault = computed(() => props.canEdit && (!props.editingTemplate || Boolean(props.editingTemplate.isActive)));
|
||||||
|
|
||||||
|
const defaultHint = computed(() => props.editingTemplate && !Boolean(props.editingTemplate.isActive)
|
||||||
|
? t('settings.adsVast.dialog.defaultDisabledHint')
|
||||||
|
: t('settings.adsVast.dialog.defaultHint'));
|
||||||
|
|
||||||
|
const adFormatLabels = computed<Record<string, string>>(() => ({
|
||||||
|
'pre-roll': t('settings.adsVast.formats.preRoll'),
|
||||||
|
'mid-roll': t('settings.adsVast.formats.midRoll'),
|
||||||
|
'post-roll': t('settings.adsVast.formats.postRoll'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateForm = (patch: Partial<CreateAdTemplateRequest>) => {
|
||||||
|
emit('update:formData', {
|
||||||
|
...props.formData,
|
||||||
|
...patch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTextField = (key: 'name' | 'vastTagUrl', value: string | number | null) => {
|
||||||
|
updateForm({
|
||||||
|
[key]: typeof value === 'string' ? value : value == null ? '' : String(value),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDuration = (value: string | number | null) => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
updateForm({ duration: value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null || value === '') {
|
||||||
|
updateForm({ duration: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
updateForm({ duration: Number.isNaN(parsed) ? undefined : parsed });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCheckbox = (event: Event) => {
|
||||||
|
updateForm({
|
||||||
|
isDefault: (event.target as HTMLInputElement).checked,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAdFormat = (format: AdFormatOption) => {
|
||||||
|
updateForm({
|
||||||
|
adFormat: format,
|
||||||
|
duration: format === 'mid-roll' ? props.formData.duration : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatButtonClass = (format: AdFormatOption) => [
|
||||||
|
'px-3 py-2 border rounded-md text-sm font-medium transition-all disabled:opacity-60 disabled:cursor-not-allowed',
|
||||||
|
props.formData.adFormat === format
|
||||||
|
? 'border-primary bg-primary/5 text-primary'
|
||||||
|
: 'border-border text-foreground/60 hover:border-primary/50',
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppDialog
|
||||||
|
:visible="visible"
|
||||||
|
:title="title"
|
||||||
|
maxWidthClass="max-w-lg"
|
||||||
|
@update:visible="emit('update:visible', $event)"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="name" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.templateName') }}</label>
|
||||||
|
<AppInput
|
||||||
|
id="name"
|
||||||
|
:model-value="formData.name"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
:placeholder="t('settings.adsVast.dialog.templateNamePlaceholder')"
|
||||||
|
@update:model-value="updateTextField('name', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label for="vastUrl" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.vastUrlLabel') }}</label>
|
||||||
|
<AppInput
|
||||||
|
id="vastUrl"
|
||||||
|
:model-value="formData.vastTagUrl"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
:placeholder="t('settings.adsVast.dialog.vastUrlPlaceholder')"
|
||||||
|
@update:model-value="updateTextField('vastTagUrl', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.adFormat') }}</label>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="format in AD_FORMAT_OPTIONS"
|
||||||
|
:key="format"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
:class="formatButtonClass(format)"
|
||||||
|
@click="selectAdFormat(format)"
|
||||||
|
>
|
||||||
|
{{ adFormatLabels[format] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="formData.adFormat === 'mid-roll'" class="grid gap-2">
|
||||||
|
<label for="duration" class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.adInterval') }}</label>
|
||||||
|
<AppInput
|
||||||
|
id="duration"
|
||||||
|
:model-value="formData.duration"
|
||||||
|
:disabled="!canEdit"
|
||||||
|
type="number"
|
||||||
|
:placeholder="t('settings.adsVast.dialog.adIntervalPlaceholder')"
|
||||||
|
:min="10"
|
||||||
|
:max="600"
|
||||||
|
@update:model-value="updateDuration"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<label class="text-sm font-medium text-foreground">{{ t('settings.adsVast.dialog.defaultLabel') }}</label>
|
||||||
|
<label
|
||||||
|
:class="[
|
||||||
|
'flex items-start gap-3 rounded-md border border-border p-3',
|
||||||
|
canToggleDefault && !saving ? 'cursor-pointer' : 'opacity-60 cursor-not-allowed',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="Boolean(formData.isDefault)"
|
||||||
|
type="checkbox"
|
||||||
|
class="mt-1 h-4 w-4 rounded border-border"
|
||||||
|
:disabled="!canToggleDefault || saving"
|
||||||
|
@change="updateCheckbox($event)"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-foreground">{{ t('settings.adsVast.dialog.defaultCheckbox') }}</p>
|
||||||
|
<p class="mt-0.5 text-xs text-foreground/60">{{ defaultHint }}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<AppButton variant="secondary" size="sm" :disabled="saving" @click="emit('close')">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</AppButton>
|
||||||
|
<AppButton size="sm" :loading="saving" :disabled="!canEdit" @click="emit('save')">
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ editingTemplate ? t('settings.adsVast.dialog.update') : t('settings.adsVast.dialog.create') }}
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AppDialog>
|
||||||
|
</template>
|
||||||
26
src/routes/settings/AdsVast/components/AdsVastNotices.vue
Normal file
26
src/routes/settings/AdsVast/components/AdsVastNotices.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SettingsNotice from '@/routes/settings/components/SettingsNotice.vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
isFreePlan: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsNotice class="rounded-none border-x-0 border-t-0 p-3" contentClass="text-xs text-foreground/70">
|
||||||
|
{{ t('settings.adsVast.infoBanner') }}
|
||||||
|
</SettingsNotice>
|
||||||
|
|
||||||
|
<SettingsNotice
|
||||||
|
v-if="isFreePlan"
|
||||||
|
tone="warning"
|
||||||
|
:title="t('settings.adsVast.readOnlyTitle')"
|
||||||
|
class="rounded-none border-x-0 border-t-0 p-3"
|
||||||
|
contentClass="text-xs text-foreground/70"
|
||||||
|
>
|
||||||
|
{{ t('settings.adsVast.readOnlyMessage') }}
|
||||||
|
</SettingsNotice>
|
||||||
|
</template>
|
||||||
252
src/routes/settings/AdsVast/components/AdsVastTable.tsx
Normal file
252
src/routes/settings/AdsVast/components/AdsVastTable.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { defineComponent, computed, type PropType } from 'vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table';
|
||||||
|
import type { AdTemplate } from '../types';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||||
|
import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||||
|
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||||
|
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||||
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
|
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||||
|
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||||
|
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'AdTemplateTable',
|
||||||
|
props: {
|
||||||
|
templates: { type: Array as PropType<AdTemplate[]>, required: true },
|
||||||
|
isInitialLoading: { type: Boolean, default: false },
|
||||||
|
isReadOnly: { type: Boolean, default: false },
|
||||||
|
isMutating: { type: Boolean, default: false },
|
||||||
|
saving: { type: Boolean, default: false },
|
||||||
|
deletingId: { type: String as PropType<string | null>, default: null },
|
||||||
|
togglingId: { type: String as PropType<string | null>, default: null },
|
||||||
|
defaultingId: { type: String as PropType<string | null>, default: null },
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
edit: (template: AdTemplate) => true,
|
||||||
|
delete: (template: AdTemplate) => true,
|
||||||
|
'toggle-active': (payload: { template: AdTemplate; value: boolean }) => true,
|
||||||
|
'set-default': (template: AdTemplate) => true,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const toast = useAppToast();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const adFormatLabels = computed<Record<string, string>>(() => ({
|
||||||
|
'pre-roll': t('settings.adsVast.formats.preRoll'),
|
||||||
|
'mid-roll': t('settings.adsVast.formats.midRoll'),
|
||||||
|
'post-roll': t('settings.adsVast.formats.postRoll'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getAdFormatLabel = (format?: string) => adFormatLabels.value[format || ''] || format || '-';
|
||||||
|
|
||||||
|
const getAdFormatColor = (format?: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
'pre-roll': 'bg-blue-500/10 text-blue-500',
|
||||||
|
'mid-roll': 'bg-yellow-500/10 text-yellow-500',
|
||||||
|
'post-roll': 'bg-purple-500/10 text-purple-500',
|
||||||
|
};
|
||||||
|
return colors[format || ''] || 'bg-gray-500/10 text-gray-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('settings.adsVast.toast.copiedSummary'),
|
||||||
|
detail: t('settings.adsVast.toast.copiedDetail'),
|
||||||
|
life: 2000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = computed<ColumnDef<AdTemplate>[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'template',
|
||||||
|
header: t('settings.adsVast.table.template'),
|
||||||
|
accessorFn: (row) => row.name || '',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-foreground">{row.original.name || ''}</span>
|
||||||
|
{row.original.isDefault && (
|
||||||
|
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||||
|
{t('settings.adsVast.defaultBadge')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p class="mt-0.5 text-xs text-foreground/50">
|
||||||
|
{t('settings.adsVast.createdOn', { date: row.original.createdAt || '-' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||||
|
cellClass: 'px-6 py-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'format',
|
||||||
|
header: t('settings.adsVast.table.format'),
|
||||||
|
accessorFn: (row) => row.adFormat || '',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<span class={['rounded-full px-2 py-1 text-xs font-medium', getAdFormatColor(row.original.adFormat)]}>
|
||||||
|
{getAdFormatLabel(row.original.adFormat)}
|
||||||
|
</span>
|
||||||
|
{row.original.adFormat === 'mid-roll' && row.original.duration && (
|
||||||
|
<span class="ml-2 text-xs text-foreground/50">({row.original.duration}s)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||||
|
cellClass: 'px-6 py-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vastUrl',
|
||||||
|
header: t('settings.adsVast.table.vastUrl'),
|
||||||
|
accessorFn: (row) => row.vastTagUrl || '',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div class="flex max-w-[240px] items-center gap-2">
|
||||||
|
<code class="truncate text-xs text-foreground/60">{row.original.vastTagUrl || ''}</code>
|
||||||
|
<AppButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={props.isMutating || !row.original.vastTagUrl}
|
||||||
|
onClick={() => copyToClipboard(row.original.vastTagUrl || '')}
|
||||||
|
v-slots={{
|
||||||
|
icon: () => <CheckIcon class="h-4 w-4" />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||||
|
cellClass: 'px-6 py-3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: t('common.status'),
|
||||||
|
accessorFn: (row) => Number(Boolean(row.isActive)),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div class="text-center">
|
||||||
|
<AppSwitch
|
||||||
|
modelValue={Boolean(row.original.isActive)}
|
||||||
|
disabled={
|
||||||
|
props.isReadOnly ||
|
||||||
|
props.saving ||
|
||||||
|
props.deletingId !== null ||
|
||||||
|
props.defaultingId !== null ||
|
||||||
|
props.togglingId === row.original.id
|
||||||
|
}
|
||||||
|
onUpdate:modelValue={(value: boolean) => emit('toggle-active', { template: row.original, value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||||
|
cellClass: 'px-6 py-3 text-center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: t('common.actions'),
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
{row.original.isDefault ? (
|
||||||
|
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||||
|
{t('settings.adsVast.actions.default')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<AppButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
loading={props.defaultingId === row.original.id}
|
||||||
|
disabled={
|
||||||
|
props.isReadOnly ||
|
||||||
|
props.saving ||
|
||||||
|
props.deletingId !== null ||
|
||||||
|
props.togglingId !== null ||
|
||||||
|
props.defaultingId !== null ||
|
||||||
|
!Boolean(row.original.isActive)
|
||||||
|
}
|
||||||
|
onClick={() => emit('set-default', row.original)}
|
||||||
|
>
|
||||||
|
{t('settings.adsVast.actions.setDefault')}
|
||||||
|
</AppButton>
|
||||||
|
)}
|
||||||
|
<AppButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={props.isReadOnly || props.isMutating}
|
||||||
|
onClick={() => emit('edit', row.original)}
|
||||||
|
v-slots={{
|
||||||
|
icon: () => <PencilIcon class="h-4 w-4" />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={props.isReadOnly || props.isMutating}
|
||||||
|
onClick={() => emit('delete', row.original)}
|
||||||
|
v-slots={{
|
||||||
|
icon: () => <TrashIcon class="h-4 w-4 text-danger" />
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||||
|
cellClass: 'px-6 py-3 text-right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<>
|
||||||
|
{props.isInitialLoading ? (
|
||||||
|
<SettingsTableSkeleton columns={5} rows={4} />
|
||||||
|
) : (
|
||||||
|
<BaseTable
|
||||||
|
data={props.templates}
|
||||||
|
columns={columns.value}
|
||||||
|
getRowId={(row: AdTemplate, index: number) =>
|
||||||
|
row.id || `${row.name || 'template'}:${row.vastTagUrl || index}`
|
||||||
|
}
|
||||||
|
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||||
|
tableClass="w-full"
|
||||||
|
headerRowClass="bg-muted/30"
|
||||||
|
bodyRowClass="border-b border-border hover:bg-muted/30"
|
||||||
|
v-slots={{
|
||||||
|
empty: () => (
|
||||||
|
<div class="px-6 py-12 text-center">
|
||||||
|
<LinkIcon class="mx-auto mb-3 block h-10 w-10 text-foreground/30" />
|
||||||
|
<p class="mb-1 text-sm text-foreground/60">{t('settings.adsVast.emptyTitle')}</p>
|
||||||
|
<p class="text-xs text-foreground/40">{t('settings.adsVast.emptySubtitle')}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
24
src/routes/settings/AdsVast/components/AdsVastToolbar.vue
Normal file
24
src/routes/settings/AdsVast/components/AdsVastToolbar.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
||||||
|
import AppButton from '@/components/ui/AppButton.vue';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
disabled: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'create'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppButton size="sm" :disabled="disabled" @click="emit('create')">
|
||||||
|
<template #icon>
|
||||||
|
<PlusIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ t('settings.adsVast.createTemplate') }}
|
||||||
|
</AppButton>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user