Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
.env.example
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# --- Database Configuration ---
|
||||||
|
DB_USER=idp
|
||||||
|
DB_PASSWORD=secure-password # WICHTIG: Ändere dieses Passwort!
|
||||||
|
DB_NAME=ipd_db
|
||||||
|
DB_PORT=5433
|
||||||
|
|
||||||
|
# --- APP Configuration ---
|
||||||
|
DATABASE_URL="postgresql://idp:secure-password@localhost:5433/idp_db"
|
||||||
|
JWT_PRIVATE_KEY_PATH=./data/jwt_private.pem
|
||||||
|
JWT_PUBLIC_KEY_PATH=./data/jwt_public.pem
|
||||||
|
APP_CONFIG_PATH=./data/app.json
|
||||||
|
|
||||||
|
# --- CSRF Configuration ---
|
||||||
|
CSRF_TRUSTED_DOMAINS=http://localhost:5173,http://localhost:3000
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# SQLite
|
||||||
|
*.db
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/static/
|
||||||
|
/drizzle/
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tailwindStylesheet": "./src/app.css"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"context7": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@upstash/context7-mcp"],
|
||||||
|
"env": {
|
||||||
|
"DEFAULT_MINIMUM_TOKENS": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
|
||||||
|
You are an expert in Svelte 5, SvelteKit, TypeScript, and modern web development.
|
||||||
|
|
||||||
|
Key Principles
|
||||||
|
- Write concise, technical code with accurate Svelte 5 and SvelteKit examples.
|
||||||
|
- Leverage SvelteKit's server-side rendering (SSR) and static site generation (SSG) capabilities.
|
||||||
|
- Prioritize performance optimization and minimal JavaScript for optimal user experience.
|
||||||
|
- Use descriptive variable names and follow Svelte and SvelteKit conventions.
|
||||||
|
- Organize files using SvelteKit's file-based routing system.
|
||||||
|
|
||||||
|
Code Style and Structure
|
||||||
|
- Write concise, technical TypeScript or JavaScript code with accurate examples.
|
||||||
|
- Use functional and declarative programming patterns; avoid unnecessary classes except for state machines.
|
||||||
|
- Prefer iteration and modularization over code duplication.
|
||||||
|
- Structure files: component logic, markup, styles, helpers, types.
|
||||||
|
- Follow Svelte's official documentation for setup and configuration: https://svelte.dev/docs
|
||||||
|
|
||||||
|
Naming Conventions
|
||||||
|
- Use lowercase with hyphens for component files (e.g., `components/auth-form.svelte`).
|
||||||
|
- Use PascalCase for component names in imports and usage.
|
||||||
|
- Use camelCase for variables, functions, and props.
|
||||||
|
|
||||||
|
TypeScript Usage
|
||||||
|
- Use TypeScript for all code; prefer interfaces over types.
|
||||||
|
- Avoid enums; use const objects instead.
|
||||||
|
- Use functional components with TypeScript interfaces for props.
|
||||||
|
- Enable strict mode in TypeScript for better type safety.
|
||||||
|
|
||||||
|
Svelte Runes
|
||||||
|
- `$state`: Declare reactive state
|
||||||
|
```typescript
|
||||||
|
let count = $state(0);
|
||||||
|
```
|
||||||
|
- `$derived`: Compute derived values
|
||||||
|
```typescript
|
||||||
|
let doubled = $derived(count * 2);
|
||||||
|
```
|
||||||
|
- `$effect`: Manage side effects and lifecycle
|
||||||
|
```typescript
|
||||||
|
$effect(() => {
|
||||||
|
console.log(`Count is now ${count}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
- `$props`: Declare component props
|
||||||
|
```typescript
|
||||||
|
let { optionalProp = 42, requiredProp } = $props();
|
||||||
|
```
|
||||||
|
- `$bindable`: Create two-way bindable props
|
||||||
|
```typescript
|
||||||
|
let { bindableProp = $bindable() } = $props();
|
||||||
|
```
|
||||||
|
- `$inspect`: Debug reactive state (development only)
|
||||||
|
```typescript
|
||||||
|
$inspect(count);
|
||||||
|
```
|
||||||
|
|
||||||
|
UI and Styling
|
||||||
|
- Use Tailwind CSS for utility-first styling approach.
|
||||||
|
- Leverage Shadcn components for pre-built, customizable UI elements.
|
||||||
|
- Import Shadcn components from `$lib/components/ui`.
|
||||||
|
- Organize Tailwind classes using the `cn()` utility from `$lib/utils`.
|
||||||
|
- Use Svelte's built-in transition and animation features.
|
||||||
|
|
||||||
|
Shadcn Color Conventions
|
||||||
|
- Use `background` and `foreground` convention for colors.
|
||||||
|
- Define CSS variables without color space function:
|
||||||
|
```css
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
```
|
||||||
|
- Usage example:
|
||||||
|
```svelte
|
||||||
|
<div class="bg-primary text-primary-foreground">Hello</div>
|
||||||
|
```
|
||||||
|
- Key color variables:
|
||||||
|
- `--background`, `--foreground`: Default body colors
|
||||||
|
- `--muted`, `--muted-foreground`: Muted backgrounds
|
||||||
|
- `--card`, `--card-foreground`: Card backgrounds
|
||||||
|
- `--popover`, `--popover-foreground`: Popover backgrounds
|
||||||
|
- `--border`: Default border color
|
||||||
|
- `--input`: Input border color
|
||||||
|
- `--primary`, `--primary-foreground`: Primary button colors
|
||||||
|
- `--secondary`, `--secondary-foreground`: Secondary button colors
|
||||||
|
- `--accent`, `--accent-foreground`: Accent colors
|
||||||
|
- `--destructive`, `--destructive-foreground`: Destructive action colors
|
||||||
|
- `--ring`: Focus ring color
|
||||||
|
- `--radius`: Border radius for components
|
||||||
|
|
||||||
|
SvelteKit Project Structure
|
||||||
|
- Use the recommended SvelteKit project structure:
|
||||||
|
```
|
||||||
|
- src/
|
||||||
|
- lib/
|
||||||
|
- routes/
|
||||||
|
- app.html
|
||||||
|
- static/
|
||||||
|
- svelte.config.js
|
||||||
|
- vite.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Component Development
|
||||||
|
- Create .svelte files for Svelte components.
|
||||||
|
- Use .svelte.ts files for component logic and state machines.
|
||||||
|
- Implement proper component composition and reusability.
|
||||||
|
- Use Svelte's props for data passing.
|
||||||
|
- Leverage Svelte's reactive declarations for local state management.
|
||||||
|
|
||||||
|
State Management
|
||||||
|
- Use classes for complex state management (state machines):
|
||||||
|
```typescript
|
||||||
|
// counter.svelte.ts
|
||||||
|
class Counter {
|
||||||
|
count = $state(0);
|
||||||
|
incrementor = $state(1);
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.count += this.incrementor;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCount() {
|
||||||
|
this.count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetIncrementor() {
|
||||||
|
this.incrementor = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const counter = new Counter();
|
||||||
|
```
|
||||||
|
- Use in components:
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
import { counter } from './counter.svelte.ts';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={() => counter.increment()}>
|
||||||
|
Count: {counter.count}
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Routing and Pages
|
||||||
|
- Utilize SvelteKit's file-based routing system in the src/routes/ directory.
|
||||||
|
- Implement dynamic routes using [slug] syntax.
|
||||||
|
- Use load functions for server-side data fetching and pre-rendering.
|
||||||
|
- Implement proper error handling with +error.svelte pages.
|
||||||
|
|
||||||
|
Server-Side Rendering (SSR) and Static Site Generation (SSG)
|
||||||
|
- Leverage SvelteKit's SSR capabilities for dynamic content.
|
||||||
|
- Implement SSG for static pages using prerender option.
|
||||||
|
- Use the adapter-auto for automatic deployment configuration.
|
||||||
|
|
||||||
|
Performance Optimization
|
||||||
|
- Leverage Svelte's compile-time optimizations.
|
||||||
|
- Use `{#key}` blocks to force re-rendering of components when needed.
|
||||||
|
- Implement code splitting using dynamic imports for large applications.
|
||||||
|
- Profile and monitor performance using browser developer tools.
|
||||||
|
- Use `$effect.tracking()` to optimize effect dependencies.
|
||||||
|
- Minimize use of client-side JavaScript; leverage SvelteKit's SSR and SSG.
|
||||||
|
- Implement proper lazy loading for images and other assets.
|
||||||
|
|
||||||
|
Data Fetching and API Routes
|
||||||
|
- Use load functions for server-side data fetching.
|
||||||
|
- Implement proper error handling for data fetching operations.
|
||||||
|
- Create API routes in the src/routes/api/ directory.
|
||||||
|
- Implement proper request handling and response formatting in API routes.
|
||||||
|
- Use SvelteKit's hooks for global API middleware.
|
||||||
|
|
||||||
|
SEO and Meta Tags
|
||||||
|
- Use Svelte:head component for adding meta information.
|
||||||
|
- Implement canonical URLs for proper SEO.
|
||||||
|
- Create reusable SEO components for consistent meta tag management.
|
||||||
|
|
||||||
|
Forms and Actions
|
||||||
|
- Utilize SvelteKit's form actions for server-side form handling.
|
||||||
|
- Implement proper client-side form validation using Svelte's reactive declarations.
|
||||||
|
- Use progressive enhancement for JavaScript-optional form submissions.
|
||||||
|
|
||||||
|
Internationalization (i18n) with Paraglide.js
|
||||||
|
- Use Paraglide.js for internationalization: https://inlang.com/m/gerre34r/library-inlang-paraglideJs
|
||||||
|
- Install Paraglide.js: `npm install @inlang/paraglide-js`
|
||||||
|
- Set up language files in the `languages` directory.
|
||||||
|
- Use the `t` function to translate strings:
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { t } from '@inlang/paraglide-js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{t('welcome_message')}</h1>
|
||||||
|
```
|
||||||
|
- Support multiple languages and RTL layouts.
|
||||||
|
- Ensure text scaling and font adjustments for accessibility.
|
||||||
|
|
||||||
|
Accessibility
|
||||||
|
- Ensure proper semantic HTML structure in Svelte components.
|
||||||
|
- Implement ARIA attributes where necessary.
|
||||||
|
- Ensure keyboard navigation support for interactive elements.
|
||||||
|
- Use Svelte's bind:this for managing focus programmatically.
|
||||||
|
|
||||||
|
Key Conventions
|
||||||
|
1. Embrace Svelte's simplicity and avoid over-engineering solutions.
|
||||||
|
2. Use SvelteKit for full-stack applications with SSR and API routes.
|
||||||
|
3. Prioritize Web Vitals (LCP, FID, CLS) for performance optimization.
|
||||||
|
4. Use environment variables for configuration management.
|
||||||
|
5. Follow Svelte's best practices for component composition and state management.
|
||||||
|
6. Ensure cross-browser compatibility by testing on multiple platforms.
|
||||||
|
7. Keep your Svelte and SvelteKit versions up to date.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
- Svelte 5 Runes: https://svelte-5-preview.vercel.app/docs/runes
|
||||||
|
- Svelte Documentation: https://svelte.dev/docs
|
||||||
|
- SvelteKit Documentation: https://kit.svelte.dev/docs
|
||||||
|
- Paraglide.js Documentation: https://inlang.com/m/gerre34r/library-inlang-paraglideJs/usage
|
||||||
|
|
||||||
|
Refer to Svelte, SvelteKit, and Paraglide.js documentation for detailed information on components, internationalization, and best practices.
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["inlang.vs-code-extension"]
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install all dependencies (including dev dependencies for build)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create a dummy .env file for build
|
||||||
|
RUN echo 'DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"' > .env && \
|
||||||
|
echo 'JWT_PRIVATE_KEY_PATH=./data/jwt_private.pem' >> .env && \
|
||||||
|
echo 'JWT_PUBLIC_KEY_PATH=./data/jwt_public.pem' >> .env && \
|
||||||
|
echo 'APP_CONFIG_PATH=./data/app.json' >> .env
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
#RUN npx drizzle-kit generate
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY --from=build /app/package*.json ./
|
||||||
|
COPY --from=build /app/drizzle ./drizzle
|
||||||
|
|
||||||
|
# Install only production dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy built application
|
||||||
|
COPY --from=build /app/build ./build
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "build/index.js"]
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# RFID-CP - RFID Card Management System
|
||||||
|
|
||||||
|
A modern web application for managing RFID cards and member access control built with Svelte 5, PostgreSQL, and Drizzle ORM.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔐 **Admin Authentication**: Secure login system for administrators
|
||||||
|
- 👥 **Member Management**: Create and manage member profiles
|
||||||
|
- 🆔 **RFID Card Management**: Track and assign RFID cards to members
|
||||||
|
- 📱 **Web NFC Integration**: Scan RFID cards directly from browsers that support Web NFC
|
||||||
|
- 🎨 **Modern UI**: Built with Shadcn/ui components and Tailwind CSS
|
||||||
|
- 🌍 **Internationalization**: Multi-language support with Paraglide JS
|
||||||
|
- 📊 **Dashboard**: Overview of system statistics and recent activity
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- PostgreSQL database
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd rfid-cp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set up environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` and configure your database connection:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://username:password@localhost:5432/rfid_cp"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set up the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Push schema to database
|
||||||
|
npm run db:push
|
||||||
|
|
||||||
|
# Run migrations (if needed)
|
||||||
|
npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Seed the database with initial data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
|
||||||
|
- An admin user with credentials: `admin` / `admin123`
|
||||||
|
- Sample members, RFID cards, and assignments
|
||||||
|
- Sample devices
|
||||||
|
|
||||||
|
6. Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Database Seeding
|
||||||
|
|
||||||
|
The seeder script creates initial data for development and testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### What gets seeded:
|
||||||
|
|
||||||
|
- **Admin User**: `admin` / `admin123`
|
||||||
|
- **Sample Members**: Max Mustermann, Anna Schmidt, Peter Müller
|
||||||
|
- **RFID Cards**: Various cards with different statuses (NEW, ENGRAVED)
|
||||||
|
- **Assignments**: Cards assigned to members
|
||||||
|
- **Devices**: RFID scanner and lock system devices
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Start development server
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run preview` - Preview production build
|
||||||
|
- `npm run check` - Type checking
|
||||||
|
- `npm run lint` - Lint code
|
||||||
|
- `npm run format` - Format code
|
||||||
|
- `npm run db:push` - Push schema to database
|
||||||
|
- `npm run db:migrate` - Run migrations
|
||||||
|
- `npm run db:studio` - Open Drizzle Studio
|
||||||
|
- `npm run db:seed` - Seed database with initial data
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── lib/
|
||||||
|
│ ├── components/ # Reusable UI components
|
||||||
|
│ ├── server/ # Server-side utilities
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
├── routes/ # SvelteKit routes
|
||||||
|
│ ├── admin/ # Admin panel routes
|
||||||
|
│ └── api/ # API routes
|
||||||
|
├── schemas/ # Database schemas and validation
|
||||||
|
└── rpc-routes/ # RPC route handlers
|
||||||
|
|
||||||
|
scripts/
|
||||||
|
└── seed.ts # Database seeding script
|
||||||
|
|
||||||
|
drizzle/ # Database migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: Svelte 5, SvelteKit
|
||||||
|
- **Styling**: Tailwind CSS, Shadcn/ui
|
||||||
|
- **Database**: PostgreSQL with Drizzle ORM
|
||||||
|
- **Authentication**: Argon2 password hashing
|
||||||
|
- **Internationalization**: Paraglide JS
|
||||||
|
- **Forms**: SvelteKit Superforms
|
||||||
|
- **Icons**: Lucide Svelte
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
The database schema is defined in `src/schemas/database/schema.ts` using Drizzle ORM. Key entities:
|
||||||
|
|
||||||
|
- `managementUsers` - Admin users
|
||||||
|
- `members` - System members
|
||||||
|
- `rfidCards` - RFID card inventory
|
||||||
|
- `memberRfidCards` - Card-to-member assignments
|
||||||
|
- `devices` - RFID scanners and lock systems
|
||||||
|
- `accessLogs` - Access event logging
|
||||||
|
- `systemLogs` - System event logging
|
||||||
|
|
||||||
|
### Web NFC Support
|
||||||
|
|
||||||
|
The application includes Web NFC integration for scanning RFID cards directly in supported browsers. The feature gracefully degrades when Web NFC is not available.
|
||||||
|
|
||||||
|
### Internationalization
|
||||||
|
|
||||||
|
The app supports multiple languages using Paraglide JS. Translation files are located in the `messages/` directory.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Add your license here]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
# Service name for our PostgreSQL database
|
||||||
|
db:
|
||||||
|
# We use a specific version of PostgreSQL for stability.
|
||||||
|
# The 'alpine' tag means it's a smaller image size.
|
||||||
|
image: postgres:16-alpine
|
||||||
|
|
||||||
|
# This ensures the database container restarts automatically if it crashes or the server reboots.
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# Environment variables are loaded from the .env file.
|
||||||
|
# These are used by the PostgreSQL image to create the user and database on first run.
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: ${DB_NAME}
|
||||||
|
|
||||||
|
# Port mapping: <HOST_PORT>:<CONTAINER_PORT>
|
||||||
|
# - We expose the internal container port 5432 (PostgreSQL's default)
|
||||||
|
# - to our custom port ${DB_PORT} (5433 from our .env) on our local machine.
|
||||||
|
ports:
|
||||||
|
- '${DB_PORT}:5432'
|
||||||
|
|
||||||
|
# Volumes are essential to persist data.
|
||||||
|
# - We create a named volume 'postgres_data'.
|
||||||
|
# - All database files inside the container (/var/lib/postgresql/data) are stored in this volume.
|
||||||
|
# - This means your data is safe even if you remove the container with 'docker-compose down'.
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# A healthcheck to ensure the database is ready to accept connections
|
||||||
|
# before other services (if any) might try to connect.
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Top-level volumes definition. This is where Docker manages the named volume.
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://idp:secure-password@db:5432/idp_db
|
||||||
|
- JWT_PRIVATE_KEY_PATH=./data/jwt_private.pem
|
||||||
|
- JWT_PUBLIC_KEY_PATH=./data/jwt_public.pem
|
||||||
|
- APP_CONFIG_PATH=./data/app.json
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
networks:
|
||||||
|
- rfid-network
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: idp
|
||||||
|
POSTGRES_PASSWORD: secure-password
|
||||||
|
POSTGRES_DB: idp_db
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U idp -d idp_db']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- rfid-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rfid-network:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "My SvelteKit1323",
|
||||||
|
"version": "1.0.013",
|
||||||
|
"supportEmail": "support@example.com",
|
||||||
|
"footerText": "© 2024 RFID Control Panel. All rights reserved."
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"enableDeviceApi": true,
|
||||||
|
"enableRFID": true,
|
||||||
|
"enableLogging": false,
|
||||||
|
"maxLoginAttempts": 5,
|
||||||
|
"selfServicePortal": {
|
||||||
|
"enabled": false,
|
||||||
|
"allowProfileUpdate": false,
|
||||||
|
"allowPasswordChange": false
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"save": ["ACTION", "INFO", "WARNING", "ERROR"],
|
||||||
|
"console": ["ACTION", "INFO", "WARNING", "ERROR"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDKIbHlA6W5GTZP
|
||||||
|
wt6X4+0xpfdMs1LyTuOAUBzZYtZ04eLlSjn05mkDxAhgzY2zI67ZPRcP1NYJvhtS
|
||||||
|
iSdF/4+DB2Q/8+InWFytAdOMWZyPRggoP9W2V7B6/Qmfco94GwsGXBzH/yzqhVDf
|
||||||
|
RB7yZ5c9+BdsWcPSQMR5r48enWOkzPZnwnN52TVvQPW9trf4CF0NBvk9jTTJyf1I
|
||||||
|
6TZJ/biNQylWoK62xuv8ONe+ewSqc0I+gNyyb7cPow9KQ0QbdSa5i8AI3v6pYvHU
|
||||||
|
7+tBBp1U5HrIaxAtrjxkcO1lB7ZrKBeJK2dmgljOn9za4BmcQrwFSjlrP37qBLkO
|
||||||
|
J/WD3nupAgMBAAECggEAQx6BNezKIPawRDgPNhynbK8RuVRqnK95d+giTEU3tUQ0
|
||||||
|
1bKaICpKgMediJd5EB0vOE/C0qz4DfAknnmQovvDqdzQezP+ytXaoFBU8OvowuKc
|
||||||
|
XtEjpBLmNVDE2z9tSJKiTJBVtkuTnKR8QEcch53CaP9yt7XvAmgjVczoF2Bn8BbQ
|
||||||
|
NqIfEuTuWlh25wQrh+ghxxnrIAXABSkNPBO3qBMIOuHz7VLiPNQYjdBdoLptyTEG
|
||||||
|
luawWpvzaBkoJxkdiswS2KaauzohlQ/c7SE353l1Of1eB39bB+wkf3nzh7CGKW+M
|
||||||
|
8/jg/Eb0fQflsAeGlV4NNpcE488rRfVoNHTxem+NqQKBgQDoyvK9lbvhU/9xyb9w
|
||||||
|
6C/hLLk3TZI5BPC2YVpYwF916q0dnhP74+pjVYg1w9vZwiKOEnIaK6N+Plrnn6l9
|
||||||
|
rhFoSzfR5n9kv3TPG+4ZiEkv42z8M0Alj4XFc9T22QStxY267s0xLtTQip9CO6wK
|
||||||
|
AAVJnZxHCHwy7f5Nzh4HN0hNTwKBgQDeSEAgjTp20VLdJqGJsK6P4iR27ulQYsBD
|
||||||
|
SqTFuv5pnunpMP/kol4tG5AByuhm4gYf/5N8YGXOwY8cNKTHQHUEXnQTYUBrCJG4
|
||||||
|
gk32+wwYKdfEiuS6fVLhGWx5JqHeNEaNl6QNjH6KE9BjYXokNa1h+bXbrBi8LDNs
|
||||||
|
td+PHX8ZhwKBgAs0CS6akCot8rM1NGNoqTU7A8bnksvvsu30DXcL+wwfuSkdvHR4
|
||||||
|
6YTSTvXXcTMvpp4TwS4FP58JvTI8etmzkN7mD8+oOiVNGYAGJhVQe8U0OsCAbuvf
|
||||||
|
1l5ETtF6bEE4qrN+Xp2pVVCb+0IiwQrSKW77iNPaUq/YyE5SRxuty2r9AoGAQBwq
|
||||||
|
krjpAdgBxFMeCC5zSoB47+ycUlkJBt+Cgp0aP7Bb2qwNQg4qh2wJrtqtCO9rwNLf
|
||||||
|
4OGUu3tMIWB1nhpTJb1wUR6di8Fe9g5vGiryJA39c2xz5+25d77zcEXaLdJ5/uCb
|
||||||
|
qmS5Im3wjplQtxzcMwPolcEfKTa+Zj5WilqBjAUCgYBmRZjkc+jZ1S5byQ0TcoIz
|
||||||
|
tbjE8kxKXCN0TrauMYPfEpNGZPUic+gOEVUD9D367Lr+foTBLA88Nal9UoK+/vxZ
|
||||||
|
kooGy7N3oPHpkErxTEMzKirAiJwDwvy8G8Mz5tle/P2vdhqX0jdeq3CUyXypSKZP
|
||||||
|
izPns90mdDLKqb7jlIgJMA==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyiGx5QOluRk2T8Lel+Pt
|
||||||
|
MaX3TLNS8k7jgFAc2WLWdOHi5Uo59OZpA8QIYM2NsyOu2T0XD9TWCb4bUoknRf+P
|
||||||
|
gwdkP/PiJ1hcrQHTjFmcj0YIKD/Vtlewev0Jn3KPeBsLBlwcx/8s6oVQ30Qe8meX
|
||||||
|
PfgXbFnD0kDEea+PHp1jpMz2Z8Jzedk1b0D1vba3+AhdDQb5PY00ycn9SOk2Sf24
|
||||||
|
jUMpVqCutsbr/DjXvnsEqnNCPoDcsm+3D6MPSkNEG3UmuYvACN7+qWLx1O/rQQad
|
||||||
|
VOR6yGsQLa48ZHDtZQe2aygXiStnZoJYzp/c2uAZnEK8BUo5az9+6gS5Dif1g957
|
||||||
|
qQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit';
|
||||||
|
|
||||||
|
// if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: './src/schemas/database/schema.ts',
|
||||||
|
dialect: 'postgresql',
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL || 'postgresql://dummy:dummy@localhost:5432/dummy'
|
||||||
|
},
|
||||||
|
verbose: true,
|
||||||
|
strict: true
|
||||||
|
});
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
CREATE TYPE "public"."card_assignment_status" AS ENUM('ASSIGNED', 'RETURNED');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."device_type" AS ENUM('RFID_SCANNER', 'LOCK_SYSTEM');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."management_user_role" AS ENUM('ADMIN');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."rfid_card_status" AS ENUM('NEW', 'ENGRAVED', 'LOST', 'DISPOSED');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."system_log_type" AS ENUM('ACTION', 'INFO', 'WARNING', 'ERROR');--> statement-breakpoint
|
||||||
|
CREATE TABLE "access_logs" (
|
||||||
|
"member_id" uuid NOT NULL,
|
||||||
|
"device_id" uuid NOT NULL,
|
||||||
|
"accessed_at" timestamp NOT NULL,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "devices" (
|
||||||
|
"name" varchar(200) NOT NULL,
|
||||||
|
"api_key" varchar(255) NOT NULL,
|
||||||
|
"type" "device_type" DEFAULT 'RFID_SCANNER' NOT NULL,
|
||||||
|
"last_seen_at" timestamp,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "devices_api_key_unique" UNIQUE("api_key")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "management_users" (
|
||||||
|
"first_name" varchar(100) NOT NULL,
|
||||||
|
"last_name" varchar(100) NOT NULL,
|
||||||
|
"username" varchar(50) NOT NULL,
|
||||||
|
"password_hash" varchar(255) NOT NULL,
|
||||||
|
"role" "management_user_role" DEFAULT 'ADMIN' NOT NULL,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "management_users_username_unique" UNIQUE("username")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "member_rfid_cards" (
|
||||||
|
"member_id" uuid NOT NULL,
|
||||||
|
"card_id" uuid NOT NULL,
|
||||||
|
"status" "card_assignment_status" DEFAULT 'ASSIGNED' NOT NULL,
|
||||||
|
"issued_at" timestamp,
|
||||||
|
"returned_at" timestamp,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "members" (
|
||||||
|
"first_name" varchar(100) NOT NULL,
|
||||||
|
"last_name" varchar(100) NOT NULL,
|
||||||
|
"title" varchar(20),
|
||||||
|
"birth_date" timestamp,
|
||||||
|
"membership_number" varchar(50),
|
||||||
|
"occupation" varchar(200),
|
||||||
|
"street" varchar(200),
|
||||||
|
"house_number" varchar(10),
|
||||||
|
"postal_code" varchar(10),
|
||||||
|
"city" varchar(100),
|
||||||
|
"phone_home" varchar(20),
|
||||||
|
"phone_work" varchar(20),
|
||||||
|
"phone_mobile" varchar(20),
|
||||||
|
"email" varchar(255),
|
||||||
|
"guest_account" boolean DEFAULT false NOT NULL,
|
||||||
|
"joined_at" timestamp,
|
||||||
|
"free_text_function" text,
|
||||||
|
"free_text_comment" text,
|
||||||
|
"password_hash" varchar(255),
|
||||||
|
"profile_image" varchar(500),
|
||||||
|
"allow_self_service" boolean DEFAULT false NOT NULL,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "members_membership_number_unique" UNIQUE("membership_number")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "rfid_cards" (
|
||||||
|
"rfid_id" varchar(50) NOT NULL,
|
||||||
|
"status" "rfid_card_status" DEFAULT 'NEW' NOT NULL,
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "rfid_cards_rfid_id_unique" UNIQUE("rfid_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "system_logs" (
|
||||||
|
"type" "system_log_type" NOT NULL,
|
||||||
|
"name" varchar(255),
|
||||||
|
"user_id" uuid,
|
||||||
|
"payload" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "access_logs" ADD CONSTRAINT "access_logs_member_id_members_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."members"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "access_logs" ADD CONSTRAINT "access_logs_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member_rfid_cards" ADD CONSTRAINT "member_rfid_cards_member_id_members_id_fk" FOREIGN KEY ("member_id") REFERENCES "public"."members"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "member_rfid_cards" ADD CONSTRAINT "member_rfid_cards_card_id_rfid_cards_id_fk" FOREIGN KEY ("card_id") REFERENCES "public"."rfid_cards"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "system_logs" ADD CONSTRAINT "system_logs_user_id_management_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."management_users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "access_logs_member_id_idx" ON "access_logs" USING btree ("member_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "access_logs_device_id_idx" ON "access_logs" USING btree ("device_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "access_logs_accessed_at_idx" ON "access_logs" USING btree ("accessed_at");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "devices_api_key_idx" ON "devices" USING btree ("api_key");--> statement-breakpoint
|
||||||
|
CREATE INDEX "devices_type_idx" ON "devices" USING btree ("type");--> statement-breakpoint
|
||||||
|
CREATE INDEX "devices_name_idx" ON "devices" USING btree ("name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "management_users_username_idx" ON "management_users" USING btree ("username");--> statement-breakpoint
|
||||||
|
CREATE INDEX "management_users_enabled_idx" ON "management_users" USING btree ("enabled");--> statement-breakpoint
|
||||||
|
CREATE INDEX "member_rfid_cards_member_id_idx" ON "member_rfid_cards" USING btree ("member_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "member_rfid_cards_card_id_idx" ON "member_rfid_cards" USING btree ("card_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "member_rfid_cards_status_idx" ON "member_rfid_cards" USING btree ("status");--> statement-breakpoint
|
||||||
|
CREATE INDEX "member_rfid_cards_member_card_status_idx" ON "member_rfid_cards" USING btree ("member_id","card_id","status");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "members_membership_number_idx" ON "members" USING btree ("membership_number");--> statement-breakpoint
|
||||||
|
CREATE INDEX "members_email_idx" ON "members" USING btree ("email");--> statement-breakpoint
|
||||||
|
CREATE INDEX "members_guest_account_idx" ON "members" USING btree ("guest_account");--> statement-breakpoint
|
||||||
|
CREATE INDEX "members_name_idx" ON "members" USING btree ("first_name","last_name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "rfid_cards_rfid_id_idx" ON "rfid_cards" USING btree ("rfid_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "rfid_cards_status_idx" ON "rfid_cards" USING btree ("status");--> statement-breakpoint
|
||||||
|
CREATE INDEX "system_logs_type_idx" ON "system_logs" USING btree ("type");--> statement-breakpoint
|
||||||
|
CREATE INDEX "system_logs_user_id_idx" ON "system_logs" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "system_logs_created_at_idx" ON "system_logs" USING btree ("created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "system_logs_name_idx" ON "system_logs" USING btree ("name");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758824141426,
|
||||||
|
"tag": "0000_peaceful_susan_delgado",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
import globals from 'globals';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
// highlight-start
|
||||||
|
// 1. Plugin importieren
|
||||||
|
import unusedImports from 'eslint-plugin-unused-imports';
|
||||||
|
// highlight-end
|
||||||
|
import svelteConfig from './svelte.config.js';
|
||||||
|
|
||||||
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
...svelte.configs.prettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node }
|
||||||
|
},
|
||||||
|
// highlight-start
|
||||||
|
// 2. Plugin hier hinzufügen
|
||||||
|
plugins: {
|
||||||
|
'unused-imports': unusedImports
|
||||||
|
},
|
||||||
|
// 3. Regeln für das Plugin definieren
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
'no-unused-vars': 'off', // Wichtig: Die Standard-ESLint-Regel deaktivieren
|
||||||
|
'unused-imports/no-unused-imports': 'error', // Regel des Plugins aktivieren
|
||||||
|
'unused-imports/no-unused-vars': [
|
||||||
|
// Sicherstellen, dass keine benutzten Variablen fälschlich entfernt werden
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
vars: 'all',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// highlight-end
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
extraFileExtensions: ['.svelte'],
|
||||||
|
parser: ts.parser,
|
||||||
|
svelteConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,545 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"example_message": "Guten Tag {username}",
|
||||||
|
"user_list_updated": "Benutzerliste wurde mit Ihren Filtern aktualisiert.",
|
||||||
|
"filters_cleared": "Alle Filter wurden gelöscht.",
|
||||||
|
"delete_user_failed": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"user_deleted_success": "Benutzer erfolgreich gelöscht.",
|
||||||
|
"user_deleted_named": "{firstName} {lastName} wurde gelöscht.",
|
||||||
|
"user_created_success": "Benutzer erfolgreich erstellt.",
|
||||||
|
"user_updated_success": "Benutzer erfolgreich aktualisiert.",
|
||||||
|
"user_create_failed": "Benutzer konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"user_update_failed": "Benutzer konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"user_delete_failed_retry": "Benutzer konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"user_id_required": "Benutzer-ID ist erforderlich",
|
||||||
|
"user_not_found": "Benutzer nicht gefunden",
|
||||||
|
"users_title": "Benutzer",
|
||||||
|
"users_description": "Benutzerkonten und Berechtigungen verwalten.",
|
||||||
|
"filter_button": "Filtern",
|
||||||
|
"add_user_button": "Benutzer hinzufügen",
|
||||||
|
"user_management_title": "Benutzerverwaltung",
|
||||||
|
"user_management_description": "Alle Benutzerkonten anzeigen und verwalten",
|
||||||
|
"search_users_placeholder": "Benutzer suchen...",
|
||||||
|
"select_label": "Auswählen",
|
||||||
|
"user_column": "Benutzer",
|
||||||
|
"role_column": "Rolle",
|
||||||
|
"status_column": "Status",
|
||||||
|
"created_column": "Erstellt",
|
||||||
|
"actions_column": "Aktionen",
|
||||||
|
"enabled_status": "Aktiviert",
|
||||||
|
"disabled_status": "Deaktiviert",
|
||||||
|
"view_details": "Details anzeigen",
|
||||||
|
"edit_user": "Benutzer bearbeiten",
|
||||||
|
"delete_user": "Benutzer löschen",
|
||||||
|
"no_users_found": "Keine Benutzer gefunden.",
|
||||||
|
"add_user_title": "Benutzer hinzufügen",
|
||||||
|
"edit_user_title": "Benutzer bearbeiten",
|
||||||
|
"create_user_description": "Einen neuen Verwaltungsbenutzer erstellen",
|
||||||
|
"modify_user_description": "Benutzerdetails ändern",
|
||||||
|
"first_name_label": "Vorname",
|
||||||
|
"last_name_label": "Nachname",
|
||||||
|
"username_label": "Benutzername",
|
||||||
|
"password_label": "Passwort",
|
||||||
|
"leave_blank_to_keep": "(leer lassen zum Beibehalten)",
|
||||||
|
"enabled_label": "Aktiviert",
|
||||||
|
"cancel_button": "Abbrechen",
|
||||||
|
"create_button": "Erstellen",
|
||||||
|
"save_button": "Speichern",
|
||||||
|
"advanced_filters_title": "Erweiterte Filter",
|
||||||
|
"advanced_filters_description": "Benutzer filtern und sortieren, um das Gewünschte zu finden",
|
||||||
|
"account_status_label": "Kontostatus",
|
||||||
|
"all_status": "Alle Status",
|
||||||
|
"enabled": "Aktiviert",
|
||||||
|
"disabled": "Deaktiviert",
|
||||||
|
"role_label": "Rolle",
|
||||||
|
"all_roles": "Alle Rollen",
|
||||||
|
"admin": "Admin",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"created_date_range_label": "Erstellungsdatumsbereich",
|
||||||
|
"from_label": "Von",
|
||||||
|
"to_label": "Bis",
|
||||||
|
"sort_by_label": "Sortieren nach",
|
||||||
|
"first_name": "Vorname",
|
||||||
|
"last_name": "Nachname",
|
||||||
|
"created_date": "Erstellungsdatum",
|
||||||
|
"ascending": "Aufsteigend",
|
||||||
|
"descending": "Absteigend",
|
||||||
|
"reset_button": "Zurücksetzen",
|
||||||
|
"apply_filters_button": "Filter anwenden",
|
||||||
|
"dashboard_title": "Dashboard",
|
||||||
|
"dashboard_welcome": "Willkommen zurück! Hier ist, was in Ihrer Anwendung passiert.",
|
||||||
|
"more_button": "Mehr",
|
||||||
|
"add_new_button": "Neu hinzufügen",
|
||||||
|
"total_users": "Gesamtbenutzer",
|
||||||
|
"revenue": "Umsatz",
|
||||||
|
"growth_rate": "Wachstumsrate",
|
||||||
|
"active_sessions": "Aktive Sitzungen",
|
||||||
|
"active_users_this_month": "Aktive Benutzer diesen Monat",
|
||||||
|
"total_revenue_this_month": "Gesamtumsatz diesen Monat",
|
||||||
|
"monthly_growth_rate": "Monatliche Wachstumsrate",
|
||||||
|
"current_active_sessions": "Aktuelle aktive Sitzungen",
|
||||||
|
"recent_activity": "Kürzliche Aktivität",
|
||||||
|
"created_new_post": "Neuen Beitrag erstellt",
|
||||||
|
"updated_profile": "Profil aktualisiert",
|
||||||
|
"deleted_comment": "Kommentar gelöscht",
|
||||||
|
"uploaded_image": "Bild hochgeladen",
|
||||||
|
"changed_settings": "Einstellungen geändert",
|
||||||
|
"welcome_to_sveltekit": "Willkommen bei SvelteKit",
|
||||||
|
"visit_docs": "Besuchen Sie svelte.dev/docs/kit, um die Dokumentation zu lesen",
|
||||||
|
"analytics_overview": "Analyse-Übersicht",
|
||||||
|
"monthly_performance_metrics": "Monatliche Leistungsmetriken und Trends",
|
||||||
|
"chart_visualization_placeholder": "Diagramm-Visualisierung würde hier stehen",
|
||||||
|
"chart_integration_needed": "Integration mit Diagramm-Bibliothek erforderlich",
|
||||||
|
"latest_user_actions": "Neueste Benutzeraktionen und Systemereignisse",
|
||||||
|
"view_all_activity": "Alle Aktivitäten anzeigen",
|
||||||
|
"quick_actions": "Schnellaktionen",
|
||||||
|
"frequently_used_tasks": "Häufig verwendete Verwaltungsaufgaben",
|
||||||
|
"manage_users": "Benutzer verwalten",
|
||||||
|
"view_reports": "Berichte anzeigen",
|
||||||
|
"system_status": "Systemstatus",
|
||||||
|
"add_content": "Inhalt hinzufügen",
|
||||||
|
"more_options": "Weitere Optionen",
|
||||||
|
"username_required": "Der Benutzername ist erforderlich.",
|
||||||
|
"first_name_required": "Der Vorname ist erforderlich.",
|
||||||
|
"last_name_required": "Der Nachname ist erforderlich.",
|
||||||
|
"password_required": "Passwort ist erforderlich.",
|
||||||
|
"password_min_length": "Das Passwort muss mindestens 8 Zeichen lang sein.",
|
||||||
|
"sidebar_dashboard": "Dashboard",
|
||||||
|
"sidebar_users": "Benutzer",
|
||||||
|
"sidebar_members": "Mitglieder",
|
||||||
|
"sidebar_devices": "Geräte",
|
||||||
|
"sidebar_analytics": "Analyse",
|
||||||
|
"sidebar_content": "Inhalt",
|
||||||
|
"sidebar_posts": "Beiträge",
|
||||||
|
"sidebar_pages": "Seiten",
|
||||||
|
"sidebar_media": "Medien",
|
||||||
|
"sidebar_database": "Datenbank",
|
||||||
|
"sidebar_settings": "Einstellungen",
|
||||||
|
"sidebar_security": "Sicherheit",
|
||||||
|
"sidebar_notifications": "Benachrichtigungen",
|
||||||
|
"sidebar_navigation": "Navigation",
|
||||||
|
"sidebar_system": "System",
|
||||||
|
"sidebar_admin_panel": "Admin Panel",
|
||||||
|
"sidebar_management": "Verwaltung",
|
||||||
|
"sidebar_system_online": "System Online",
|
||||||
|
"settings_title": "Einstellungen",
|
||||||
|
"settings_description": "Konfigurieren Sie Ihre Anwendungseinstellungen",
|
||||||
|
"general_settings": "Allgemeine Einstellungen",
|
||||||
|
"site_name": "Seitenname",
|
||||||
|
"site_name_placeholder": "Geben Sie den Seitennamen ein",
|
||||||
|
"site_description": "Seitenbeschreibung",
|
||||||
|
"site_description_placeholder": "Beschreiben Sie Ihre Anwendung",
|
||||||
|
"admin_email": "Admin-E-Mail",
|
||||||
|
"admin_email_placeholder": "admin@example.com",
|
||||||
|
"timezone": "Zeitzone",
|
||||||
|
"timezone_placeholder": "UTC",
|
||||||
|
"language": "Sprache",
|
||||||
|
"language_placeholder": "de",
|
||||||
|
"security_settings": "Sicherheitseinstellungen",
|
||||||
|
"two_factor_auth": "Zwei-Faktor-Authentifizierung",
|
||||||
|
"session_timeout": "Sitzungs-Timeout (Minuten)",
|
||||||
|
"password_complexity": "Passwort-Komplexität",
|
||||||
|
"login_attempts": "Maximale Anmeldeversuche",
|
||||||
|
"ip_whitelist": "IP-Whitelist",
|
||||||
|
"notification_settings": "Benachrichtigungseinstellungen",
|
||||||
|
"email_notifications": "E-Mail-Benachrichtigungen",
|
||||||
|
"push_notifications": "Push-Benachrichtigungen",
|
||||||
|
"weekly_reports": "Wöchentliche Berichte",
|
||||||
|
"system_alerts": "Systemwarnungen",
|
||||||
|
"user_registrations": "Benutzerregistrierungen",
|
||||||
|
"system_settings": "Systemeinstellungen",
|
||||||
|
"maintenance_mode": "Wartungsmodus",
|
||||||
|
"backup_frequency": "Sicherungshäufigkeit",
|
||||||
|
"backup_frequency_placeholder": "täglich",
|
||||||
|
"save_settings": "Einstellungen speichern",
|
||||||
|
"settings_saved": "Einstellungen erfolgreich gespeichert",
|
||||||
|
"settings_save_error": "Fehler beim Speichern der Einstellungen",
|
||||||
|
"confirm_delete": "Löschen bestätigen",
|
||||||
|
"confirm_delete_description": "Sind Sie sicher, dass Sie {firstName} {lastName} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"confirm_delete_generic": "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"user_details_title": "Benutzerdetails",
|
||||||
|
"user_information": "Benutzerinformationen",
|
||||||
|
"no_user_selected": "Kein Benutzer ausgewählt",
|
||||||
|
"edit_user_button": "Benutzer bearbeiten",
|
||||||
|
"close_button": "Schließen",
|
||||||
|
"delete_user_button": "Benutzer löschen",
|
||||||
|
"user_role": "Rolle",
|
||||||
|
"user_status": "Status",
|
||||||
|
"created_label": "Erstellt",
|
||||||
|
"last_updated": "Zuletzt aktualisiert",
|
||||||
|
"user_id_label": "Benutzer-ID",
|
||||||
|
"select_all_users": "Alle Benutzer auswählen",
|
||||||
|
"selected_count": "{count} von {total} ausgewählt",
|
||||||
|
"select_users_bulk": "Benutzer für Massenaktionen auswählen",
|
||||||
|
"bulk_actions": "Massenaktionen",
|
||||||
|
"actions_for_users": "Aktionen für {count} Benutzer",
|
||||||
|
"enable_users": "Benutzer aktivieren",
|
||||||
|
"disable_users": "Benutzer deaktivieren",
|
||||||
|
"send_email": "E-Mail senden",
|
||||||
|
"delete_users": "Benutzer löschen",
|
||||||
|
"total_users_stat": "Gesamtbenutzer",
|
||||||
|
"all_registered_users": "Alle registrierten Benutzer",
|
||||||
|
"currently_enabled": "Derzeit aktiviert",
|
||||||
|
"accounts_disabled": "Konten deaktiviert",
|
||||||
|
"admins_stat": "Administratoren",
|
||||||
|
"administrator_accounts": "Administrator-Konten",
|
||||||
|
"device_list_updated": "Geräteliste wurde mit Ihren Filtern aktualisiert.",
|
||||||
|
"delete_device_failed": "Gerät konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"device_deleted_success": "Gerät erfolgreich gelöscht.",
|
||||||
|
"device_deleted_named": "{name} wurde gelöscht.",
|
||||||
|
"device_created_success": "Gerät erfolgreich erstellt.",
|
||||||
|
"device_updated_success": "Gerät erfolgreich aktualisiert.",
|
||||||
|
"device_create_failed": "Gerät konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"device_update_failed": "Gerät konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"device_delete_failed_retry": "Gerät konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"device_id_required": "Geräte-ID ist erforderlich",
|
||||||
|
"device_not_found": "Gerät nicht gefunden",
|
||||||
|
"devices_title": "Geräte",
|
||||||
|
"devices_description": "RFID-Geräte und Schließsysteme verwalten.",
|
||||||
|
"add_device_button": "Gerät hinzufügen",
|
||||||
|
"total_devices_stat": "Gesamtgeräte",
|
||||||
|
"all_registered_devices": "Alle registrierten Geräte",
|
||||||
|
"rfid_scanners_stat": "RFID-Scanner",
|
||||||
|
"rfid_scanner_devices": "RFID-Scanner-Geräte",
|
||||||
|
"lock_systems_stat": "Schließsysteme",
|
||||||
|
"lock_system_devices": "Schließsystem-Geräte",
|
||||||
|
"device_management_title": "Geräteverwaltung",
|
||||||
|
"device_management_description": "Alle registrierten Geräte anzeigen und verwalten",
|
||||||
|
"search_devices_placeholder": "Geräte suchen...",
|
||||||
|
"device_column": "Gerät",
|
||||||
|
"type_column": "Typ",
|
||||||
|
"api_key_column": "API-Schlüssel",
|
||||||
|
"last_seen_column": "Zuletzt gesehen",
|
||||||
|
"rfid_scanner": "RFID-Scanner",
|
||||||
|
"lock_system": "Schließsystem",
|
||||||
|
"edit_device": "Gerät bearbeiten",
|
||||||
|
"delete_device": "Gerät löschen",
|
||||||
|
"no_devices_found": "Keine Geräte gefunden.",
|
||||||
|
"add_device_title": "Gerät hinzufügen",
|
||||||
|
"edit_device_title": "Gerät bearbeiten",
|
||||||
|
"create_device_description": "Ein neues Gerät erstellen",
|
||||||
|
"modify_device_description": "Gerätedetails ändern",
|
||||||
|
"device_name_label": "Gerätename",
|
||||||
|
"device_type_label": "Gerätetyp",
|
||||||
|
"api_key_label": "API-Schlüssel (optional)",
|
||||||
|
"leave_blank_for_auto_generation": "Leer lassen für automatische Generierung",
|
||||||
|
"api_key_auto_generation_description": "Falls nicht angegeben, wird ein API-Schlüssel automatisch generiert",
|
||||||
|
"confirm_delete_device_description": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"delete_device_button": "Gerät löschen",
|
||||||
|
"device_details_title": "Gerätedetails",
|
||||||
|
"device_information": "Geräteinformationen anzeigen",
|
||||||
|
"name_label": "Name",
|
||||||
|
"type_label": "Typ",
|
||||||
|
"last_seen_label": "Zuletzt gesehen",
|
||||||
|
"updated_label": "Aktualisiert",
|
||||||
|
"no_device_selected": "Kein Gerät ausgewählt",
|
||||||
|
"edit_device_button": "Gerät bearbeiten",
|
||||||
|
"advanced_filters_device_description": "Geräte nach Typ und anderen Kriterien filtern",
|
||||||
|
"all_types": "Alle Typen",
|
||||||
|
"member_list_updated": "Mitgliederliste wurde mit Ihren Filtern aktualisiert.",
|
||||||
|
"delete_member_failed": "Mitglied konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"member_deleted_success": "Mitglied erfolgreich gelöscht.",
|
||||||
|
"member_deleted_named": "{name} wurde gelöscht.",
|
||||||
|
"member_created_success": "Mitglied erfolgreich erstellt.",
|
||||||
|
"member_updated_success": "Mitglied erfolgreich aktualisiert.",
|
||||||
|
"member_create_failed": "Mitglied konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"member_update_failed": "Mitglied konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"member_delete_failed_retry": "Mitglied konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"member_id_required": "Mitglieds-ID ist erforderlich",
|
||||||
|
"member_not_found": "Mitglied nicht gefunden",
|
||||||
|
"form_check_errors": "Bitte überprüfen Sie das Formular auf Fehler",
|
||||||
|
"members_title": "Mitglieder",
|
||||||
|
"members_description": "Vereinsmitglieder und deren Informationen verwalten.",
|
||||||
|
"add_member_button": "Mitglied hinzufügen",
|
||||||
|
"total_members_stat": "Gesamtmitglieder",
|
||||||
|
"all_registered_members": "Alle registrierten Mitglieder",
|
||||||
|
"guest_accounts_stat": "Gastkonten",
|
||||||
|
"guest_member_accounts": "Gastmitglieder-Konten",
|
||||||
|
"regular_members_stat": "Reguläre Mitglieder",
|
||||||
|
"regular_member_accounts": "Reguläre Mitglieder-Konten",
|
||||||
|
"member_management_title": "Mitgliederverwaltung",
|
||||||
|
"member_management_description": "Alle Vereinsmitglieder anzeigen und verwalten",
|
||||||
|
"search_members_placeholder": "Mitglieder suchen...",
|
||||||
|
"membership_number_column": "Mitgliedsnr.",
|
||||||
|
"email_column": "E-Mail",
|
||||||
|
"phone_column": "Telefon",
|
||||||
|
"edit_member": "Mitglied bearbeiten",
|
||||||
|
"delete_member": "Mitglied löschen",
|
||||||
|
"no_members_found": "Keine Mitglieder gefunden.",
|
||||||
|
"add_member_title": "Mitglied hinzufügen",
|
||||||
|
"edit_member_title": "Mitglied bearbeiten",
|
||||||
|
"create_member_description": "Ein neues Mitglied erstellen",
|
||||||
|
"modify_member_description": "Mitgliederdetails ändern",
|
||||||
|
"membership_number_label": "Mitgliedsnummer",
|
||||||
|
"title_label": "Titel",
|
||||||
|
"birth_date_label": "Geburtsdatum",
|
||||||
|
"occupation_label": "Beruf",
|
||||||
|
"street_label": "Straße",
|
||||||
|
"house_number_label": "Hausnummer",
|
||||||
|
"postal_code_label": "Postleitzahl",
|
||||||
|
"city_label": "Stadt",
|
||||||
|
"phone_home_label": "Telefon (Privat)",
|
||||||
|
"phone_work_label": "Telefon (Arbeit)",
|
||||||
|
"phone_mobile_label": "Telefon (Mobil)",
|
||||||
|
"guest_account_label": "Gastkonto",
|
||||||
|
"joined_at_label": "Beigetreten am",
|
||||||
|
"free_text_function_label": "Funktion/Rolle",
|
||||||
|
"free_text_comment_label": "Kommentare",
|
||||||
|
"confirm_delete_member_description": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"delete_member_button": "Mitglied löschen",
|
||||||
|
"member_details_title": "Mitgliederdetails",
|
||||||
|
"member_information": "Mitgliederinformationen anzeigen",
|
||||||
|
"no_member_selected": "Kein Mitglied ausgewählt",
|
||||||
|
"edit_member_button": "Mitglied bearbeiten",
|
||||||
|
"advanced_filters_member_description": "Mitglieder nach verschiedenen Kriterien filtern",
|
||||||
|
"account_type_label": "Kontotyp",
|
||||||
|
"all_account_types": "Alle Kontotypen",
|
||||||
|
"guest_account": "Gastkonto",
|
||||||
|
"member_account": "Mitgliedskonto",
|
||||||
|
"joined_date_range_label": "Beitrittsdatumsbereich",
|
||||||
|
"birth_date_range_label": "Geburtsdatumsbereich",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"membership_number": "Mitgliedsnummer",
|
||||||
|
"all_members": "Alle Mitglieder",
|
||||||
|
"rfid_cards_title": "RFID-Karten",
|
||||||
|
"rfid_cards_description": "RFID-Karten und Zugangskontrolle verwalten.",
|
||||||
|
"add_rfid_card_button": "RFID-Karte hinzufügen",
|
||||||
|
"rfid_card_management_title": "RFID-Karten-Verwaltung",
|
||||||
|
"rfid_card_management_description": "Alle RFID-Karten anzeigen und verwalten",
|
||||||
|
"search_rfid_cards_placeholder": "RFID-Karten suchen...",
|
||||||
|
"rfid_id_column": "RFID-ID",
|
||||||
|
"assigned_member_column": "Zugewiesenes Mitglied",
|
||||||
|
"active_status": "Aktiv",
|
||||||
|
"inactive_status": "Inaktiv",
|
||||||
|
"edit_rfid_card": "RFID-Karte bearbeiten",
|
||||||
|
"delete_rfid_card": "RFID-Karte löschen",
|
||||||
|
"no_rfid_cards_found": "Keine RFID-Karten gefunden.",
|
||||||
|
"add_rfid_card_title": "RFID-Karte hinzufügen",
|
||||||
|
"edit_rfid_card_title": "RFID-Karte bearbeiten",
|
||||||
|
"create_rfid_card_description": "Eine neue RFID-Karte erstellen",
|
||||||
|
"modify_rfid_card_description": "RFID-Kartendetails ändern",
|
||||||
|
"rfid_id_label": "RFID-ID",
|
||||||
|
"rfid_card_status_label": "Status",
|
||||||
|
"rfid_card_status_new": "Neu",
|
||||||
|
"rfid_card_status_engraved": "Beschriftet",
|
||||||
|
"rfid_card_status_lost": "Verloren",
|
||||||
|
"rfid_card_status_disposed": "Entsorgt",
|
||||||
|
"assign_rfid_card_title": "RFID-Karte zuweisen",
|
||||||
|
"assign_rfid_card_description": "Diese RFID-Karte einem Mitglied zuweisen",
|
||||||
|
"unassign_rfid_card_title": "RFID-Karte-Zuweisung aufheben",
|
||||||
|
"unassign_rfid_card_description": "Die Zuweisung dieser RFID-Karte von ihrem aktuellen Mitglied entfernen",
|
||||||
|
"assign_card_button": "Karte zuweisen",
|
||||||
|
"unassign_card_button": "Zuweisung aufheben",
|
||||||
|
"select_member_label": "Mitglied auswählen",
|
||||||
|
"no_members_available": "Keine Mitglieder verfügbar",
|
||||||
|
"rfid_card_assigned_success": "RFID-Karte erfolgreich zugewiesen",
|
||||||
|
"rfid_card_unassigned_success": "RFID-Karte-Zuweisung erfolgreich aufgehoben",
|
||||||
|
"rfid_card_assign_failed": "RFID-Karte konnte nicht zugewiesen werden",
|
||||||
|
"rfid_card_unassign_failed": "RFID-Karte-Zuweisung konnte nicht aufgehoben werden",
|
||||||
|
"assigned_member_label": "Zugewiesenes Mitglied",
|
||||||
|
"select_member_placeholder": "Ein Mitglied auswählen",
|
||||||
|
"unassigned": "Nicht zugewiesen",
|
||||||
|
"confirm_delete_rfid_card_description": "Sind Sie sicher, dass Sie die RFID-Karte \"{rfidId}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"delete_rfid_card_button": "RFID-Karte löschen",
|
||||||
|
"rfid_card_details_title": "RFID-Kartendetails",
|
||||||
|
"rfid_card_information": "RFID-Karteninformationen anzeigen",
|
||||||
|
"no_rfid_card_selected": "Keine RFID-Karte ausgewählt",
|
||||||
|
"edit_rfid_card_button": "RFID-Karte bearbeiten",
|
||||||
|
"advanced_filters_rfid_card_description": "RFID-Karten nach Status und anderen Kriterien filtern",
|
||||||
|
"all_statuses": "Alle Status",
|
||||||
|
"rfid_card_list_updated": "RFID-Kartenliste wurde mit Ihren Filtern aktualisiert.",
|
||||||
|
"delete_rfid_card_failed": "RFID-Karte konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"rfid_card_deleted_success": "RFID-Karte erfolgreich gelöscht.",
|
||||||
|
"rfid_card_deleted_named": "RFID-Karte {rfidId} wurde gelöscht.",
|
||||||
|
"rfid_card_created_success": "RFID-Karte erfolgreich erstellt.",
|
||||||
|
"rfid_card_updated_success": "RFID-Karte erfolgreich aktualisiert.",
|
||||||
|
"rfid_card_create_failed": "RFID-Karte konnte nicht erstellt werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"rfid_card_update_failed": "RFID-Karte konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"rfid_card_delete_failed_retry": "RFID-Karte konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"rfid_card_id_required": "RFID-Karten-ID ist erforderlich",
|
||||||
|
"rfid_card_not_found": "RFID-Karte nicht gefunden",
|
||||||
|
"total_rfid_cards_stat": "Gesamt-RFID-Karten",
|
||||||
|
"all_registered_rfid_cards": "Alle registrierten RFID-Karten",
|
||||||
|
"active_rfid_cards_stat": "Aktive RFID-Karten",
|
||||||
|
"active_rfid_card_count": "Anzahl aktiver RFID-Karten",
|
||||||
|
"inactive_rfid_cards_stat": "Inaktive RFID-Karten",
|
||||||
|
"inactive_rfid_card_count": "Anzahl inaktiver RFID-Karten",
|
||||||
|
"assigned_rfid_cards_stat": "Zugewiesene RFID-Karten",
|
||||||
|
"assigned_rfid_card_count": "Anzahl zugewiesener RFID-Karten",
|
||||||
|
"unassigned_rfid_cards_stat": "Nicht zugewiesene RFID-Karten",
|
||||||
|
"unassigned_rfid_card_count": "Anzahl nicht zugewiesener RFID-Karten",
|
||||||
|
"sidebar_rfid_cards": "RFID-Karten",
|
||||||
|
"sidebar_rfid_card_assignments": "Zuweisungen",
|
||||||
|
"sidebar_logs": "Protokolle",
|
||||||
|
"sidebar_system_logs": "Systemprotokoll",
|
||||||
|
"sidebar_access_logs": "Zutrittsprotokolle",
|
||||||
|
"rfid_card_assignments_title": "RFID-Karten-Zuweisungen",
|
||||||
|
"access_logs_title": "Zutrittsprotokolle",
|
||||||
|
"access_logs_description": "Zutrittsprotokolle für RFID-System anzeigen",
|
||||||
|
"accessed_at_column": "Zugang um",
|
||||||
|
"search_access_logs_placeholder": "Zutrittsprotokolle suchen...",
|
||||||
|
"no_access_logs_found": "Keine Zutrittsprotokolle gefunden.",
|
||||||
|
"advanced_filters_access_logs_description": "Zutrittsprotokolle nach verschiedenen Kriterien filtern",
|
||||||
|
"from_date_label": "Von Datum",
|
||||||
|
"to_date_label": "Bis Datum",
|
||||||
|
"rfid_card_assignments_description": "RFID-Karten-Zuweisungen zu Mitgliedern verwalten",
|
||||||
|
"rfid_card_assignments_table_title": "RFID-Karten-Zuweisungen",
|
||||||
|
"rfid_card_assignments_table_description": "RFID-Karten-Zuweisungen zu Mitgliedern anzeigen und verwalten",
|
||||||
|
"rfid_card_column": "RFID-Karte",
|
||||||
|
"assignment_status_column": "Status",
|
||||||
|
"issued_at_column": "Ausgegeben am",
|
||||||
|
"returned_at_column": "Zurückgegeben am",
|
||||||
|
"assigned_status": "Zugewiesen",
|
||||||
|
"returned_status": "Zurückgegeben",
|
||||||
|
"unknown_member": "Unbekanntes Mitglied",
|
||||||
|
"unknown_card": "Unbekannt",
|
||||||
|
"no_assignments_found": "Keine Zuweisungen gefunden",
|
||||||
|
"admin_login_title": "Admin-Anmeldung",
|
||||||
|
"admin_login_subtitle": "Zugriff auf die RFID-Control-Panel-Verwaltung",
|
||||||
|
"sign_in": "Anmelden",
|
||||||
|
"enter_credentials": "Geben Sie Ihre Anmeldedaten ein, um auf das Admin-Panel zuzugreifen",
|
||||||
|
"username": "Benutzername",
|
||||||
|
"password": "Passwort",
|
||||||
|
"enter_username": "Geben Sie Ihren Benutzernamen ein",
|
||||||
|
"enter_password": "Geben Sie Ihr Passwort ein",
|
||||||
|
"signing_in": "Anmeldung läuft",
|
||||||
|
"secure_admin_access": "Sicherer Admin-Zugang erforderlich",
|
||||||
|
"all_rights_reserved": "Alle Rechte vorbehalten",
|
||||||
|
"zod": {
|
||||||
|
"duplicate_membership_number": "Mitgliedsnummer ist bereits vergeben.",
|
||||||
|
"duplicate_email": "E-Mail-Adresse ist bereits registriert.",
|
||||||
|
"field_required": "Dieses Feld ist erforderlich",
|
||||||
|
"username_required": "Benutzername ist erforderlich",
|
||||||
|
"first_name_required": "Vorname ist erforderlich",
|
||||||
|
"last_name_required": "Nachname ist erforderlich",
|
||||||
|
"password_required": "Passwort ist erforderlich",
|
||||||
|
"email_required": "E-Mail ist erforderlich",
|
||||||
|
"invalid_field": "Ungültiger Wert",
|
||||||
|
"invalid_email": "Ungültige E-Mail-Adresse",
|
||||||
|
"invalid_username": "Ungültiger Benutzername",
|
||||||
|
"password_min_length": "Passwort muss mindestens 8 Zeichen lang sein",
|
||||||
|
"string_too_short": "Text ist zu kurz",
|
||||||
|
"string_too_long": "Text ist zu lang",
|
||||||
|
"number_too_small": "Zahl ist zu klein",
|
||||||
|
"number_too_big": "Zahl ist zu groß",
|
||||||
|
"invalid_date": "Ungültiges Datum",
|
||||||
|
"invalid_string": "Ungültiger Zeichenkettenwert",
|
||||||
|
"array_too_short": "Array muss mindestens {min} Elemente enthalten",
|
||||||
|
"array_too_long": "Array darf höchstens {max} Elemente enthalten",
|
||||||
|
"value_too_small": "Wert ist zu klein",
|
||||||
|
"value_too_big": "Wert ist zu groß",
|
||||||
|
"invalid_url": "Ungültiges URL-Format",
|
||||||
|
"invalid_uuid": "Ungültiges UUID-Format",
|
||||||
|
"invalid_cuid": "Ungültiges CUID-Format",
|
||||||
|
"invalid_ulid": "Ungültiges ULID-Format",
|
||||||
|
"invalid_format": "Ungültiges Format",
|
||||||
|
"not_multiple_of": "Wert muss ein Vielfaches von {multiple} sein",
|
||||||
|
"unrecognized_keys": "Unbekannte Schlüssel: {keys}",
|
||||||
|
"invalid_union": "Ungültiger Union-Wert",
|
||||||
|
"invalid_key": "Ungültiger Schlüssel: {key}",
|
||||||
|
"invalid_element": "Ungültiges Array-Element",
|
||||||
|
"invalid_value": "Ungültiger Wert",
|
||||||
|
"passwords_do_not_match": "Passwörter stimmen nicht überein",
|
||||||
|
"password_mismatch": "Passwörter stimmen nicht überein",
|
||||||
|
"username_already_exists": "Benutzername ist bereits vergeben",
|
||||||
|
"email_already_exists": "E-Mail ist bereits registriert",
|
||||||
|
"identifier_required": "E-Mail oder Mitgliedsnummer ist erforderlich",
|
||||||
|
"token_required": "Reset-Token ist erforderlich",
|
||||||
|
"current_password_required": "Aktuelles Passwort ist erforderlich",
|
||||||
|
"invalid_credentials": "Ungültige E-Mail/Mitgliedsnummer oder Passwort",
|
||||||
|
"account_disabled": "Konto ist deaktiviert",
|
||||||
|
"self_service_disabled": "Selbstbedienungszugang ist für dieses Konto nicht aktiviert",
|
||||||
|
"birth_date_in_future" : "Geburtsdatum kann nicht in der Zukunft liegen"
|
||||||
|
},
|
||||||
|
"selfservice": {
|
||||||
|
"member_portal": "Mitgliederportal",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"my_profile": "Mein Profil",
|
||||||
|
"security": "Sicherheit",
|
||||||
|
"profile_title": "Mein Profil",
|
||||||
|
"profile_description": "Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten",
|
||||||
|
"profile_updated_success": "Profil erfolgreich aktualisiert!",
|
||||||
|
"profile_update_failed": "Profil konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"account_information": "Kontoinformationen",
|
||||||
|
"account_info_description": "Informationen, die nicht über den Selbstbedienungsbereich geändert werden können",
|
||||||
|
"status": "Status",
|
||||||
|
"guest_member": "Gastmitglied",
|
||||||
|
"full_member": "Vollmitglied",
|
||||||
|
"membership_number": "Mitgliedsnummer",
|
||||||
|
"date_of_birth": "Geburtsdatum",
|
||||||
|
"title": "Titel",
|
||||||
|
"occupation": "Beruf",
|
||||||
|
"member_since": "Mitglied seit",
|
||||||
|
"admin_only_fields": "Diese Felder können nur von einem Administrator aktualisiert werden",
|
||||||
|
"personal_information": "Persönliche Informationen",
|
||||||
|
"personal_info_description": "Aktualisieren Sie Ihre persönlichen Daten und Kontaktinformationen",
|
||||||
|
"personal_details": "Persönliche Daten",
|
||||||
|
"first_name": "Vorname",
|
||||||
|
"last_name": "Nachname",
|
||||||
|
"enter_first_name": "Geben Sie Ihren Vornamen ein",
|
||||||
|
"enter_last_name": "Geben Sie Ihren Nachnamen ein",
|
||||||
|
"contact_information": "Kontaktinformationen",
|
||||||
|
"email_address": "E-Mail-Adresse",
|
||||||
|
"enter_email": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||||
|
"home_phone": "Festnetz",
|
||||||
|
"work_phone": "Arbeitstelefon",
|
||||||
|
"mobile_phone": "Mobiltelefon",
|
||||||
|
"address": "Adresse",
|
||||||
|
"street": "Straße",
|
||||||
|
"street_name": "Straßenname",
|
||||||
|
"number": "Nummer",
|
||||||
|
"house_number": "Hausnr.",
|
||||||
|
"postal_code": "Postleitzahl",
|
||||||
|
"city": "Stadt",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"save_changes": "Änderungen speichern",
|
||||||
|
"saving": "Speichern...",
|
||||||
|
"welcome_back": "Willkommen zurück",
|
||||||
|
"sign_in_description": "Melden Sie sich mit Ihrer E-Mail oder Mitgliedsnummer an",
|
||||||
|
"email_or_membership": "E-Mail oder Mitgliedsnummer",
|
||||||
|
"enter_email_or_membership": "Geben Sie Ihre E-Mail oder Mitgliedsnummer ein",
|
||||||
|
"password": "Passwort",
|
||||||
|
"enter_password": "Geben Sie Ihr Passwort ein",
|
||||||
|
"forgot_password": "Passwort vergessen?",
|
||||||
|
"signing_in": "Anmeldung läuft...",
|
||||||
|
"sign_in": "Anmelden",
|
||||||
|
"no_selfservice_access": "Haben Sie keinen Zugang zum Selbstbedienungsportal?",
|
||||||
|
"contact_admin": "Wenden Sie sich an Ihren Organisationsadministrator.",
|
||||||
|
"admin_login": "Admin-Anmeldung",
|
||||||
|
"access_selfservice_portal": "Greifen Sie auf Ihr Selbstbedienungsportal zu",
|
||||||
|
"hide_password": "Passwort ausblenden",
|
||||||
|
"show_password": "Passwort anzeigen",
|
||||||
|
"profile_update": "Profilaktualisierung",
|
||||||
|
"not_set": "Nicht festgelegt",
|
||||||
|
"dashboard_title": "Dashboard",
|
||||||
|
"manage_profile_description": "Verwalten Sie Ihr Profil und greifen Sie auf Selbstbedienungsfunktionen zu",
|
||||||
|
"member_profile": "Mitgliederprofil",
|
||||||
|
"current_member_info": "Ihre aktuellen Mitgliederinformationen",
|
||||||
|
"no_title_set": "Kein Titel festgelegt",
|
||||||
|
"edit_profile": "Profil bearbeiten",
|
||||||
|
"quick_actions": "Schnellaktionen",
|
||||||
|
"common_tasks_shortcuts": "Häufige Aufgaben und Verknüpfungen",
|
||||||
|
"update_personal_info": "Persönliche Informationen aktualisieren",
|
||||||
|
"change_password": "Passwort ändern",
|
||||||
|
"account_status": "Kontostatus",
|
||||||
|
"your_account_info": "Ihre Kontoinformationen",
|
||||||
|
"account_type": "Kontotyp",
|
||||||
|
"self_service_access": "Selbstbedienungszugang",
|
||||||
|
"enabled": "Aktiviert",
|
||||||
|
"need_help": "Brauchen Sie Hilfe?",
|
||||||
|
"contact_support_assistance": "Support kontaktieren oder Hilfe erhalten",
|
||||||
|
"need_assistance_text": "Wenn Sie Hilfe bei Ihrem Konto benötigen oder Fragen haben, wenden Sie sich bitte an Ihren Organisationsadministrator.",
|
||||||
|
"contact_support": "Support kontaktieren",
|
||||||
|
"security_settings": "Sicherheitseinstellungen",
|
||||||
|
"manage_security_password": "Verwalten Sie die Sicherheit Ihres Kontos und Ihr Passwort",
|
||||||
|
"update_password_secure": "Aktualisieren Sie Ihr Passwort, um Ihr Konto sicher zu halten",
|
||||||
|
"current_password": "Aktuelles Passwort",
|
||||||
|
"enter_current_password": "Aktuelles Passwort eingeben",
|
||||||
|
"new_password": "Neues Passwort",
|
||||||
|
"enter_new_password": "Neues Passwort eingeben",
|
||||||
|
"confirm_new_password": "Neues Passwort bestätigen",
|
||||||
|
"confirm_new_password_placeholder": "Neues Passwort bestätigen",
|
||||||
|
"updating": "Aktualisierung läuft...",
|
||||||
|
"update_password": "Passwort aktualisieren"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"example_message": "Hello world {username}",
|
||||||
|
"user_list_updated": "User list has been updated with your filters.",
|
||||||
|
"filters_cleared": "All filters have been cleared.",
|
||||||
|
"delete_user_failed": "Failed to delete user. Please try again.",
|
||||||
|
"user_deleted_success": "User deleted successfully.",
|
||||||
|
"user_deleted_named": "{firstName} {lastName} has been deleted.",
|
||||||
|
"user_created_success": "User created successfully.",
|
||||||
|
"user_updated_success": "User updated successfully.",
|
||||||
|
"user_create_failed": "Failed to create user. Please try again.",
|
||||||
|
"user_update_failed": "Failed to update user. Please try again.",
|
||||||
|
"user_delete_failed_retry": "Failed to delete user. Please try again.",
|
||||||
|
"user_id_required": "User ID is required",
|
||||||
|
"user_not_found": "User not found",
|
||||||
|
"users_title": "Users",
|
||||||
|
"users_description": "Manage user accounts and permissions.",
|
||||||
|
"filter_button": "Filter",
|
||||||
|
"add_user_button": "Add User",
|
||||||
|
"user_management_title": "User Management",
|
||||||
|
"user_management_description": "View and manage all user accounts",
|
||||||
|
"search_users_placeholder": "Search users...",
|
||||||
|
"select_label": "Select",
|
||||||
|
"user_column": "User",
|
||||||
|
"role_column": "Role",
|
||||||
|
"status_column": "Status",
|
||||||
|
"created_column": "Created",
|
||||||
|
"actions_column": "Actions",
|
||||||
|
"enabled_status": "Enabled",
|
||||||
|
"disabled_status": "Disabled",
|
||||||
|
"view_details": "View details",
|
||||||
|
"edit_user": "Edit user",
|
||||||
|
"delete_user": "Delete user",
|
||||||
|
"no_users_found": "No users found.",
|
||||||
|
"add_user_title": "Add User",
|
||||||
|
"edit_user_title": "Edit User",
|
||||||
|
"create_user_description": "Create a new management user",
|
||||||
|
"modify_user_description": "Modify user details",
|
||||||
|
"first_name_label": "First name",
|
||||||
|
"last_name_label": "Last name",
|
||||||
|
"username_label": "Username",
|
||||||
|
"password_label": "Password",
|
||||||
|
"leave_blank_to_keep": "(leave blank to keep)",
|
||||||
|
"enabled_label": "Enabled",
|
||||||
|
"cancel_button": "Cancel",
|
||||||
|
"create_button": "Create",
|
||||||
|
"save_button": "Save",
|
||||||
|
"advanced_filters_title": "Advanced Filters",
|
||||||
|
"advanced_filters_description": "Filter and sort users to find what you need",
|
||||||
|
"account_status_label": "Account Status",
|
||||||
|
"all_status": "All Status",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"role_label": "Role",
|
||||||
|
"all_roles": "All Roles",
|
||||||
|
"admin": "Admin",
|
||||||
|
"user": "User",
|
||||||
|
"created_date_range_label": "Created Date Range",
|
||||||
|
"from_label": "From",
|
||||||
|
"to_label": "To",
|
||||||
|
"sort_by_label": "Sort By",
|
||||||
|
"first_name": "First Name",
|
||||||
|
"last_name": "Last Name",
|
||||||
|
"created_date": "Created Date",
|
||||||
|
"ascending": "Ascending",
|
||||||
|
"descending": "Descending",
|
||||||
|
"reset_button": "Reset",
|
||||||
|
"apply_filters_button": "Apply Filters",
|
||||||
|
"dashboard_title": "Dashboard",
|
||||||
|
"dashboard_welcome": "Welcome back! Here's what's happening with your application.",
|
||||||
|
"more_button": "More",
|
||||||
|
"add_new_button": "Add New",
|
||||||
|
"total_users": "Total Users",
|
||||||
|
"revenue": "Revenue",
|
||||||
|
"growth_rate": "Growth Rate",
|
||||||
|
"active_sessions": "Active Sessions",
|
||||||
|
"active_users_this_month": "Active users this month",
|
||||||
|
"total_revenue_this_month": "Total revenue this month",
|
||||||
|
"monthly_growth_rate": "Monthly growth rate",
|
||||||
|
"current_active_sessions": "Current active sessions",
|
||||||
|
"recent_activity": "Recent Activity",
|
||||||
|
"created_new_post": "Created new post",
|
||||||
|
"updated_profile": "Updated profile",
|
||||||
|
"deleted_comment": "Deleted comment",
|
||||||
|
"uploaded_image": "Uploaded image",
|
||||||
|
"changed_settings": "Changed settings",
|
||||||
|
"welcome_to_sveltekit": "Welcome to SvelteKit",
|
||||||
|
"visit_docs": "Visit svelte.dev/docs/kit to read the documentation",
|
||||||
|
"analytics_overview": "Analytics Overview",
|
||||||
|
"monthly_performance_metrics": "Monthly performance metrics and trends",
|
||||||
|
"chart_visualization_placeholder": "Chart visualization would go here",
|
||||||
|
"chart_integration_needed": "Integration with charting library needed",
|
||||||
|
"latest_user_actions": "Latest user actions and system events",
|
||||||
|
"view_all_activity": "View All Activity",
|
||||||
|
"quick_actions": "Quick Actions",
|
||||||
|
"frequently_used_tasks": "Frequently used administrative tasks",
|
||||||
|
"manage_users": "Manage Users",
|
||||||
|
"view_reports": "View Reports",
|
||||||
|
"system_status": "System Status",
|
||||||
|
"add_content": "Add Content",
|
||||||
|
"more_options": "More Options",
|
||||||
|
"username_required": "Username is required.",
|
||||||
|
"first_name_required": "First name is required.",
|
||||||
|
"last_name_required": "Last name is required.",
|
||||||
|
"password_required": "Password is required.",
|
||||||
|
"password_min_length": "Password must be at least 8 characters long.",
|
||||||
|
"sidebar_dashboard": "Dashboard",
|
||||||
|
"sidebar_users": "Users",
|
||||||
|
"sidebar_members": "Members",
|
||||||
|
"sidebar_devices": "Devices",
|
||||||
|
"sidebar_analytics": "Analytics",
|
||||||
|
"sidebar_content": "Content",
|
||||||
|
"sidebar_posts": "Posts",
|
||||||
|
"sidebar_pages": "Pages",
|
||||||
|
"sidebar_media": "Media",
|
||||||
|
"sidebar_database": "Database",
|
||||||
|
"sidebar_settings": "Settings",
|
||||||
|
"sidebar_security": "Security",
|
||||||
|
"sidebar_notifications": "Notifications",
|
||||||
|
"sidebar_navigation": "Navigation",
|
||||||
|
"sidebar_system": "System",
|
||||||
|
"sidebar_admin_panel": "Admin Panel",
|
||||||
|
"sidebar_management": "Management",
|
||||||
|
"sidebar_system_online": "System Online",
|
||||||
|
"invalid_credentials": "Invalid username or password",
|
||||||
|
"account_disabled": "Account is disabled",
|
||||||
|
"login_failed": "Login failed",
|
||||||
|
"settings_title": "Settings",
|
||||||
|
"settings_description": "Configure your application settings",
|
||||||
|
"general_settings": "General Settings",
|
||||||
|
"site_name": "Site Name",
|
||||||
|
"site_name_placeholder": "Enter site name",
|
||||||
|
"site_description": "Site Description",
|
||||||
|
"site_description_placeholder": "Describe your application",
|
||||||
|
"admin_email": "Admin Email",
|
||||||
|
"admin_email_placeholder": "admin@example.com",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"timezone_placeholder": "UTC",
|
||||||
|
"language": "Language",
|
||||||
|
"language_placeholder": "en",
|
||||||
|
"security_settings": "Security Settings",
|
||||||
|
"two_factor_auth": "Two-Factor Authentication",
|
||||||
|
"session_timeout": "Session Timeout (Minutes)",
|
||||||
|
"password_complexity": "Password Complexity",
|
||||||
|
"login_attempts": "Maximum Login Attempts",
|
||||||
|
"ip_whitelist": "IP Whitelist",
|
||||||
|
"notification_settings": "Notification Settings",
|
||||||
|
"email_notifications": "Email Notifications",
|
||||||
|
"push_notifications": "Push Notifications",
|
||||||
|
"weekly_reports": "Weekly Reports",
|
||||||
|
"system_alerts": "System Alerts",
|
||||||
|
"user_registrations": "User Registrations",
|
||||||
|
"system_settings": "System Settings",
|
||||||
|
"maintenance_mode": "Maintenance Mode",
|
||||||
|
"backup_frequency": "Backup Frequency",
|
||||||
|
"backup_frequency_placeholder": "daily",
|
||||||
|
"save_settings": "Save Settings",
|
||||||
|
"settings_saved": "Settings saved successfully",
|
||||||
|
"settings_save_error": "Error saving settings",
|
||||||
|
"confirm_delete": "Confirm Delete",
|
||||||
|
"confirm_delete_description": "Are you sure you want to delete {firstName} {lastName}? This action cannot be undone.",
|
||||||
|
"confirm_delete_generic": "Are you sure you want to delete this user? This action cannot be undone.",
|
||||||
|
"user_details_title": "User Details",
|
||||||
|
"user_information": "User information",
|
||||||
|
"no_user_selected": "No user selected",
|
||||||
|
"edit_user_button": "Edit User",
|
||||||
|
"close_button": "Close",
|
||||||
|
"delete_user_button": "Delete User",
|
||||||
|
"user_role": "Role",
|
||||||
|
"user_status": "Status",
|
||||||
|
"created_label": "Created",
|
||||||
|
"last_updated": "Last Updated",
|
||||||
|
"user_id_label": "User ID",
|
||||||
|
"select_all_users": "Select all users",
|
||||||
|
"selected_count": "{count} of {total} selected",
|
||||||
|
"select_users_bulk": "Select users for bulk actions",
|
||||||
|
"bulk_actions": "Bulk Actions",
|
||||||
|
"actions_for_users": "Actions for {count} users",
|
||||||
|
"enable_users": "Enable Users",
|
||||||
|
"disable_users": "Disable Users",
|
||||||
|
"send_email": "Send Email",
|
||||||
|
"delete_users": "Delete Users",
|
||||||
|
"total_users_stat": "Total Users",
|
||||||
|
"all_registered_users": "All registered users",
|
||||||
|
"currently_enabled": "Currently enabled",
|
||||||
|
"accounts_disabled": "Accounts disabled",
|
||||||
|
"admins_stat": "Admins",
|
||||||
|
"administrator_accounts": "Administrator accounts",
|
||||||
|
"device_list_updated": "Device list has been updated with your filters.",
|
||||||
|
"delete_device_failed": "Failed to delete device. Please try again.",
|
||||||
|
"device_deleted_success": "Device deleted successfully.",
|
||||||
|
"device_deleted_named": "{name} has been deleted.",
|
||||||
|
"device_created_success": "Device created successfully.",
|
||||||
|
"device_updated_success": "Device updated successfully.",
|
||||||
|
"device_create_failed": "Failed to create device. Please try again.",
|
||||||
|
"device_update_failed": "Failed to update device. Please try again.",
|
||||||
|
"device_delete_failed_retry": "Failed to delete device. Please try again.",
|
||||||
|
"device_id_required": "Device ID is required",
|
||||||
|
"device_not_found": "Device not found",
|
||||||
|
"devices_title": "Devices",
|
||||||
|
"devices_description": "Manage RFID devices and lock systems.",
|
||||||
|
"add_device_button": "Add Device",
|
||||||
|
"total_devices_stat": "Total Devices",
|
||||||
|
"all_registered_devices": "All registered devices",
|
||||||
|
"rfid_scanners_stat": "RFID Scanners",
|
||||||
|
"rfid_scanner_devices": "RFID scanner devices",
|
||||||
|
"lock_systems_stat": "Lock Systems",
|
||||||
|
"lock_system_devices": "Lock system devices",
|
||||||
|
"device_management_title": "Device Management",
|
||||||
|
"device_management_description": "View and manage all registered devices",
|
||||||
|
"search_devices_placeholder": "Search devices...",
|
||||||
|
"device_column": "Device",
|
||||||
|
"type_column": "Type",
|
||||||
|
"api_key_column": "API Key",
|
||||||
|
"last_seen_column": "Last Seen",
|
||||||
|
"rfid_scanner": "RFID Scanner",
|
||||||
|
"lock_system": "Lock System",
|
||||||
|
"edit_device": "Edit device",
|
||||||
|
"delete_device": "Delete device",
|
||||||
|
"no_devices_found": "No devices found.",
|
||||||
|
"add_device_title": "Add Device",
|
||||||
|
"edit_device_title": "Edit Device",
|
||||||
|
"create_device_description": "Create a new device",
|
||||||
|
"modify_device_description": "Modify device details",
|
||||||
|
"device_name_label": "Device Name",
|
||||||
|
"device_type_label": "Device Type",
|
||||||
|
"api_key_label": "API Key (optional)",
|
||||||
|
"leave_blank_for_auto_generation": "Leave blank for auto-generation",
|
||||||
|
"api_key_auto_generation_description": "If not provided, an API key will be generated automatically",
|
||||||
|
"confirm_delete_device_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||||
|
"delete_device_button": "Delete Device",
|
||||||
|
"device_details_title": "Device Details",
|
||||||
|
"device_information": "View device information",
|
||||||
|
"name_label": "Name",
|
||||||
|
"type_label": "Type",
|
||||||
|
"last_seen_label": "Last Seen",
|
||||||
|
"updated_label": "Updated",
|
||||||
|
"no_device_selected": "No device selected",
|
||||||
|
"edit_device_button": "Edit Device",
|
||||||
|
"advanced_filters_device_description": "Filter devices by type and other criteria",
|
||||||
|
"all_types": "All Types",
|
||||||
|
"rfid_card_list_updated": "RFID card list has been updated with your filters.",
|
||||||
|
"delete_rfid_card_failed": "Failed to delete RFID card. Please try again.",
|
||||||
|
"rfid_card_deleted_success": "RFID card deleted successfully.",
|
||||||
|
"rfid_card_deleted_named": "RFID card {rfidId} has been deleted.",
|
||||||
|
"rfid_card_created_success": "RFID card created successfully.",
|
||||||
|
"rfid_card_updated_success": "RFID card updated successfully.",
|
||||||
|
"rfid_card_create_failed": "Failed to create RFID card. Please try again.",
|
||||||
|
"rfid_card_update_failed": "Failed to update RFID card. Please try again.",
|
||||||
|
"rfid_card_delete_failed_retry": "Failed to delete RFID card. Please try again.",
|
||||||
|
"rfid_card_id_required": "RFID card ID is required",
|
||||||
|
"rfid_card_not_found": "RFID card not found",
|
||||||
|
"rfid_cards_title": "RFID Cards",
|
||||||
|
"rfid_cards_description": "Manage RFID cards and their assignments.",
|
||||||
|
"add_rfid_card_button": "Add RFID Card",
|
||||||
|
"total_rfid_cards_stat": "Total RFID Cards",
|
||||||
|
"all_registered_rfid_cards": "All registered RFID cards",
|
||||||
|
"new_cards_stat": "New Cards",
|
||||||
|
"new_rfid_cards": "New RFID cards",
|
||||||
|
"engraved_cards_stat": "Engraved Cards",
|
||||||
|
"engraved_rfid_cards": "Engraved RFID cards",
|
||||||
|
"rfid_card_management_title": "RFID Card Management",
|
||||||
|
"rfid_card_management_description": "View and manage all RFID cards",
|
||||||
|
"search_rfid_cards_placeholder": "Search RFID cards...",
|
||||||
|
"rfid_id_column": "RFID ID",
|
||||||
|
"assigned_member_column": "Assigned Member",
|
||||||
|
"active_status": "Active",
|
||||||
|
"inactive_status": "Inactive",
|
||||||
|
"rfid_card_status_label": "Status",
|
||||||
|
"rfid_card_status_new": "New",
|
||||||
|
"rfid_card_status_engraved": "Engraved",
|
||||||
|
"rfid_card_status_lost": "Lost",
|
||||||
|
"rfid_card_status_disposed": "Disposed",
|
||||||
|
"assign_rfid_card_title": "Assign RFID Card",
|
||||||
|
"assign_rfid_card_description": "Assign this RFID card to a member",
|
||||||
|
"unassign_rfid_card_title": "Unassign RFID Card",
|
||||||
|
"unassign_rfid_card_description": "Remove the assignment of this RFID card from its current member",
|
||||||
|
"assign_card_button": "Assign Card",
|
||||||
|
"unassign_card_button": "Unassign Card",
|
||||||
|
"select_member_label": "Select Member",
|
||||||
|
"no_members_available": "No members available",
|
||||||
|
"rfid_card_assigned_success": "RFID card assigned successfully",
|
||||||
|
"rfid_card_unassigned_success": "RFID card unassigned successfully",
|
||||||
|
"rfid_card_assign_failed": "Failed to assign RFID card",
|
||||||
|
"rfid_card_unassign_failed": "Failed to unassign RFID card",
|
||||||
|
"assigned_member_label": "Assigned Member",
|
||||||
|
"select_member_placeholder": "Select a member",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
|
"active_rfid_cards_stat": "Active RFID Cards",
|
||||||
|
"active_rfid_card_count": "Active RFID card count",
|
||||||
|
"inactive_rfid_cards_stat": "Inactive RFID Cards",
|
||||||
|
"inactive_rfid_card_count": "Inactive RFID card count",
|
||||||
|
"assigned_rfid_cards_stat": "Assigned RFID Cards",
|
||||||
|
"assigned_rfid_card_count": "Assigned RFID card count",
|
||||||
|
"unassigned_rfid_cards_stat": "Unassigned RFID Cards",
|
||||||
|
"unassigned_rfid_card_count": "Unassigned RFID card count",
|
||||||
|
"sidebar_rfid_cards": "RFID Cards",
|
||||||
|
"sidebar_rfid_card_assignments": "Assignments",
|
||||||
|
"sidebar_logs": "Logs",
|
||||||
|
"sidebar_system_logs": "System Logs",
|
||||||
|
"sidebar_access_logs": "Access Logs",
|
||||||
|
"rfid_card_assignments_title": "RFID Card Assignments",
|
||||||
|
"access_logs_title": "Access Logs",
|
||||||
|
"access_logs_description": "View access logs for RFID system",
|
||||||
|
"accessed_at_column": "Accessed At",
|
||||||
|
"search_access_logs_placeholder": "Search access logs...",
|
||||||
|
"no_access_logs_found": "No access logs found.",
|
||||||
|
"advanced_filters_access_logs_description": "Filter access logs by various criteria",
|
||||||
|
"from_date_label": "From Date",
|
||||||
|
"to_date_label": "To Date",
|
||||||
|
"rfid_card_assignments_description": "Manage RFID card assignments to members",
|
||||||
|
"rfid_card_assignments_table_title": "RFID Card Assignments",
|
||||||
|
"rfid_card_assignments_table_description": "View and manage RFID card assignments to members",
|
||||||
|
"rfid_card_column": "RFID Card",
|
||||||
|
"assignment_status_column": "Status",
|
||||||
|
"issued_at_column": "Issued At",
|
||||||
|
"returned_at_column": "Returned At",
|
||||||
|
"assigned_status": "Assigned",
|
||||||
|
"returned_status": "Returned",
|
||||||
|
"unknown_member": "Unknown Member",
|
||||||
|
"unknown_card": "Unknown",
|
||||||
|
"no_assignments_found": "No assignments found",
|
||||||
|
"edit_rfid_card": "Edit RFID card",
|
||||||
|
"delete_rfid_card": "Delete RFID card",
|
||||||
|
"no_rfid_cards_found": "No RFID cards found.",
|
||||||
|
"add_rfid_card_title": "Add RFID Card",
|
||||||
|
"edit_rfid_card_title": "Edit RFID Card",
|
||||||
|
"create_rfid_card_description": "Create a new RFID card",
|
||||||
|
"modify_rfid_card_description": "Modify RFID card details",
|
||||||
|
"rfid_id_label": "RFID ID",
|
||||||
|
"card_status_label": "Status",
|
||||||
|
"confirm_delete_rfid_card_description": "Are you sure you want to delete RFID card \"{rfidId}\"? This action cannot be undone.",
|
||||||
|
"delete_rfid_card_button": "Delete RFID Card",
|
||||||
|
"rfid_card_details_title": "RFID Card Details",
|
||||||
|
"rfid_card_information": "View RFID card information",
|
||||||
|
"no_rfid_card_selected": "No RFID card selected",
|
||||||
|
"edit_rfid_card_button": "Edit RFID Card",
|
||||||
|
"advanced_filters_rfid_card_description": "Filter RFID cards by status and other criteria",
|
||||||
|
"all_statuses": "All Statuses",
|
||||||
|
"member_list_updated": "Member list has been updated with your filters.",
|
||||||
|
"delete_member_failed": "Failed to delete member. Please try again.",
|
||||||
|
"member_deleted_success": "Member deleted successfully.",
|
||||||
|
"member_deleted_named": "{name} has been deleted.",
|
||||||
|
"member_created_success": "Member created successfully.",
|
||||||
|
"member_updated_success": "Member updated successfully.",
|
||||||
|
"member_create_failed": "Failed to create member. Please try again.",
|
||||||
|
"member_update_failed": "Failed to update member. Please try again.",
|
||||||
|
"member_delete_failed_retry": "Failed to delete member. Please try again.",
|
||||||
|
"member_id_required": "Member ID is required",
|
||||||
|
"member_not_found": "Member not found",
|
||||||
|
"form_check_errors": "Please check the form for errors",
|
||||||
|
"members_title": "Members",
|
||||||
|
"members_description": "Manage club members and their information.",
|
||||||
|
"add_member_button": "Add Member",
|
||||||
|
"total_members_stat": "Total Members",
|
||||||
|
"all_registered_members": "All registered members",
|
||||||
|
"guest_accounts_stat": "Guest Accounts",
|
||||||
|
"guest_member_accounts": "Guest member accounts",
|
||||||
|
"regular_members_stat": "Regular Members",
|
||||||
|
"regular_member_accounts": "Regular member accounts",
|
||||||
|
"member_management_title": "Member Management",
|
||||||
|
"member_management_description": "View and manage all club members",
|
||||||
|
"search_members_placeholder": "Search members...",
|
||||||
|
"member_column": "Member",
|
||||||
|
"membership_number_column": "Membership #",
|
||||||
|
"email_column": "Email",
|
||||||
|
"phone_column": "Phone",
|
||||||
|
"edit_member": "Edit member",
|
||||||
|
"delete_member": "Delete member",
|
||||||
|
"no_members_found": "No members found.",
|
||||||
|
"add_member_title": "Add Member",
|
||||||
|
"edit_member_title": "Edit Member",
|
||||||
|
"create_member_description": "Create a new member",
|
||||||
|
"modify_member_description": "Modify member details",
|
||||||
|
"membership_number_label": "Membership Number",
|
||||||
|
"title_label": "Title",
|
||||||
|
"birth_date_label": "Birth Date",
|
||||||
|
"occupation_label": "Occupation",
|
||||||
|
"street_label": "Street",
|
||||||
|
"house_number_label": "House Number",
|
||||||
|
"postal_code_label": "Postal Code",
|
||||||
|
"city_label": "City",
|
||||||
|
"phone_home_label": "Phone (Home)",
|
||||||
|
"phone_work_label": "Phone (Work)",
|
||||||
|
"phone_mobile_label": "Phone (Mobile)",
|
||||||
|
"guest_account_label": "Guest Account",
|
||||||
|
"joined_at_label": "Joined At",
|
||||||
|
"free_text_function_label": "Function/Role",
|
||||||
|
"free_text_comment_label": "Comments",
|
||||||
|
"confirm_delete_member_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||||
|
"delete_member_button": "Delete Member",
|
||||||
|
"member_details_title": "Member Details",
|
||||||
|
"member_information": "View member information",
|
||||||
|
"no_member_selected": "No member selected",
|
||||||
|
"edit_member_button": "Edit Member",
|
||||||
|
"advanced_filters_member_description": "Filter members by various criteria",
|
||||||
|
"all_members": "All Members",
|
||||||
|
"admin_login_title": "Admin Login",
|
||||||
|
"admin_login_subtitle": "Access the RFID Control Panel administration",
|
||||||
|
"sign_in": "Sign In",
|
||||||
|
"enter_credentials": "Enter your credentials to access the admin panel",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"enter_username": "Enter your username",
|
||||||
|
"enter_password": "Enter your password",
|
||||||
|
"signing_in": "Signing in",
|
||||||
|
"secure_admin_access": "Secure admin access required",
|
||||||
|
"all_rights_reserved": "All rights reserved",
|
||||||
|
"zod": {
|
||||||
|
"duplicate_membership_number": "Membership number is already in use.",
|
||||||
|
"duplicate_email": "Email address is already registered.",
|
||||||
|
"field_required": "This field is required.",
|
||||||
|
"username_required": "Username is required.",
|
||||||
|
"first_name_required": "First name is required.",
|
||||||
|
"last_name_required": "Last name is required.",
|
||||||
|
"password_required": "Password is required.",
|
||||||
|
"email_required": "Email is required.",
|
||||||
|
"invalid_field": "Invalid value.",
|
||||||
|
"invalid_email": "Invalid email address.",
|
||||||
|
"invalid_username": "Invalid username.",
|
||||||
|
"password_min_length": "Password must be at least 8 characters long.",
|
||||||
|
"string_too_short": "Text is too short (minimum {min} characters).",
|
||||||
|
"string_too_long": "Text is too long (maximum {max} characters).",
|
||||||
|
"number_too_small": "Number is too small (minimum {min}).",
|
||||||
|
"number_too_big": "Number is too big (maximum {max}).",
|
||||||
|
"invalid_date": "Invalid date.",
|
||||||
|
"invalid_string": "Invalid string value.",
|
||||||
|
"array_too_short": "Array must contain at least {min} items.",
|
||||||
|
"array_too_long": "Array must contain at most {max} items.",
|
||||||
|
"value_too_small": "Value is too small.",
|
||||||
|
"value_too_big": "Value is too big.",
|
||||||
|
"invalid_url": "Invalid URL format.",
|
||||||
|
"invalid_uuid": "Invalid UUID format.",
|
||||||
|
"invalid_cuid": "Invalid CUID format.",
|
||||||
|
"invalid_ulid": "Invalid ULID format.",
|
||||||
|
"invalid_format": "Invalid format.",
|
||||||
|
"not_multiple_of": "Value must be a multiple of {multiple}.",
|
||||||
|
"unrecognized_keys": "Unrecognized keys: {keys}.",
|
||||||
|
"invalid_union": "Invalid union value.",
|
||||||
|
"invalid_key": "Invalid key: {key}.",
|
||||||
|
"invalid_element": "Invalid array element.",
|
||||||
|
"invalid_value": "Invalid value.",
|
||||||
|
"passwords_do_not_match": "Passwords do not match.",
|
||||||
|
"password_mismatch": "Passwords do not match.",
|
||||||
|
"username_already_exists": "This username is already taken.",
|
||||||
|
"email_already_exists": "This email address is already registered.",
|
||||||
|
"identifier_required": "Email or membership number is required.",
|
||||||
|
"token_required": "Reset token is required.",
|
||||||
|
"current_password_required": "Current password is required.",
|
||||||
|
"invalid_credentials": "Invalid email/membership number or password.",
|
||||||
|
"account_disabled": "Account is disabled.",
|
||||||
|
"self_service_disabled": "Self-service access is not enabled for this account.",
|
||||||
|
"birth_date_in_future": "Date of birth cannot be in the future."
|
||||||
|
},
|
||||||
|
"selfservice": {
|
||||||
|
"member_portal": "Member Portal",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"my_profile": "My Profile",
|
||||||
|
"security": "Security",
|
||||||
|
"profile_title": "My Profile",
|
||||||
|
"profile_description": "Manage your personal information and contact details",
|
||||||
|
"profile_updated_success": "Profile updated successfully!",
|
||||||
|
"profile_update_failed": "Failed to update profile. Please try again.",
|
||||||
|
"account_information": "Account Information",
|
||||||
|
"account_info_description": "Information that cannot be changed through self-service",
|
||||||
|
"status": "Status",
|
||||||
|
"guest_member": "Guest Member",
|
||||||
|
"full_member": "Full Member",
|
||||||
|
"membership_number": "Membership Number",
|
||||||
|
"date_of_birth": "Date of Birth",
|
||||||
|
"title": "Title",
|
||||||
|
"occupation": "Occupation",
|
||||||
|
"member_since": "Member Since",
|
||||||
|
"admin_only_fields": "These fields can only be updated by an administrator",
|
||||||
|
"personal_information": "Personal Information",
|
||||||
|
"personal_info_description": "Update your personal details and contact information",
|
||||||
|
"personal_details": "Personal Details",
|
||||||
|
"first_name": "First Name",
|
||||||
|
"last_name": "Last Name",
|
||||||
|
"enter_first_name": "Enter your first name",
|
||||||
|
"enter_last_name": "Enter your last name",
|
||||||
|
"contact_information": "Contact Information",
|
||||||
|
"email_address": "Email Address",
|
||||||
|
"enter_email": "Enter your email address",
|
||||||
|
"home_phone": "Home Phone",
|
||||||
|
"work_phone": "Work Phone",
|
||||||
|
"mobile_phone": "Mobile Phone",
|
||||||
|
"address": "Address",
|
||||||
|
"street": "Street",
|
||||||
|
"street_name": "Street name",
|
||||||
|
"number": "Number",
|
||||||
|
"house_number": "House #",
|
||||||
|
"postal_code": "Postal Code",
|
||||||
|
"city": "City",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save_changes": "Save Changes",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"welcome_back": "Welcome Back",
|
||||||
|
"sign_in_description": "Sign in with your email or membership number",
|
||||||
|
"email_or_membership": "Email or Membership Number",
|
||||||
|
"enter_email_or_membership": "Enter your email or membership number",
|
||||||
|
"password": "Password",
|
||||||
|
"enter_password": "Enter your password",
|
||||||
|
"forgot_password": "Forgot your password?",
|
||||||
|
"signing_in": "Signing in...",
|
||||||
|
"sign_in": "Sign In",
|
||||||
|
"no_selfservice_access": "Don't have access to the self-service portal?",
|
||||||
|
"contact_admin": "Contact your organization administrator.",
|
||||||
|
"admin_login": "Admin Login",
|
||||||
|
"access_selfservice_portal": "Access your self-service portal",
|
||||||
|
"hide_password": "Hide password",
|
||||||
|
"show_password": "Show password",
|
||||||
|
"profile_update": "Profile Update",
|
||||||
|
"not_set": "Not set",
|
||||||
|
"dashboard_title": "Dashboard",
|
||||||
|
"manage_profile_description": "Manage your profile and access self-service features",
|
||||||
|
"member_profile": "Member Profile",
|
||||||
|
"current_member_info": "Your current member information",
|
||||||
|
"no_title_set": "No title set",
|
||||||
|
"edit_profile": "Edit Profile",
|
||||||
|
"quick_actions": "Quick Actions",
|
||||||
|
"common_tasks_shortcuts": "Common tasks and shortcuts",
|
||||||
|
"update_personal_info": "Update Personal Info",
|
||||||
|
"change_password": "Change Password",
|
||||||
|
"account_status": "Account Status",
|
||||||
|
"your_account_info": "Your account information",
|
||||||
|
"account_type": "Account Type",
|
||||||
|
"self_service_access": "Self-Service Access",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"need_help": "Need Help?",
|
||||||
|
"contact_support_assistance": "Contact support or get assistance",
|
||||||
|
"need_assistance_text": "If you need assistance with your account or have questions, please contact your organization administrator.",
|
||||||
|
"contact_support": "Contact Support",
|
||||||
|
"security_settings": "Security Settings",
|
||||||
|
"manage_security_password": "Manage your account security and password",
|
||||||
|
"update_password_secure": "Update your password to keep your account secure",
|
||||||
|
"current_password": "Current Password",
|
||||||
|
"enter_current_password": "Enter current password",
|
||||||
|
"new_password": "New Password",
|
||||||
|
"enter_new_password": "Enter new password",
|
||||||
|
"confirm_new_password": "Confirm New Password",
|
||||||
|
"confirm_new_password_placeholder": "Confirm new password",
|
||||||
|
"updating": "Updating...",
|
||||||
|
"update_password": "Update Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+8458
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"name": "rfid-cp",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:seed": "tsx scripts/seed.ts",
|
||||||
|
"db:seed-admin": "tsx scripts/seed-admin.ts",
|
||||||
|
"docker-compose:up": "docker-compose -f ./docker-compose.yml up -d --build",
|
||||||
|
"docker-compose:down": "docker-compose -f ./docker-compose.yml down",
|
||||||
|
"machine-translate": "inlang machine translate --project project.inlang",
|
||||||
|
"proto:generate": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=src/generated/ ./proto/control_communication.proto"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.5",
|
||||||
|
"@eslint/js": "^9.22.0",
|
||||||
|
"@inlang/cli": "^3.0.0",
|
||||||
|
"@inlang/paraglide-js": "2.3.2",
|
||||||
|
"@internationalized/date": "^3.9.0",
|
||||||
|
"@lucide/svelte": "^0.515.0",
|
||||||
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
|
"@sveltejs/kit": "^2.22.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@tanstack/table-core": "^8.21.3",
|
||||||
|
"@types/argon2": "^0.14.1",
|
||||||
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
|
"@types/d3-scale": "^4.0.9",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"bits-ui": "^2.11.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dotenv": "^17.2.2",
|
||||||
|
"drizzle-kit": "^0.30.2",
|
||||||
|
"drizzle-orm": "^0.40.0",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^3.12.4",
|
||||||
|
"eslint-plugin-unused-imports": "^4.2.0",
|
||||||
|
"formsnap": "^2.0.1",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"layerchart": "^2.0.0-next.39",
|
||||||
|
"mdsvex": "^0.12.3",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-sonner": "^1.0.5",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwind-variants": "^1.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"tw-animate-css": "^1.3.8",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@orpc/client": "^1.8.9",
|
||||||
|
"@orpc/json-schema": "^1.8.9",
|
||||||
|
"@orpc/openapi": "^1.8.9",
|
||||||
|
"@orpc/server": "^1.8.9",
|
||||||
|
"@orpc/tanstack-query": "^1.8.9",
|
||||||
|
"@orpc/zod": "^1.8.9",
|
||||||
|
"@tanstack/svelte-query": "^5.89.0",
|
||||||
|
"argon2": "^0.44.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"drizzle-zod": "^0.8.3",
|
||||||
|
"jose": "^6.1.0",
|
||||||
|
"postgres": "^3.4.7",
|
||||||
|
"sveltekit-superforms": "^2.27.1",
|
||||||
|
"ts-proto": "^2.7.7",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"zod": "4.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cache
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
8jHSxX0rZat6wCtV9T
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/project-settings",
|
||||||
|
"baseLocale": "en",
|
||||||
|
"locales": ["en", "de"],
|
||||||
|
"modules": [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||||
|
],
|
||||||
|
"plugin.inlang.messageFormat": {
|
||||||
|
"pathPattern": "./messages/{locale}.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package control_communication;
|
||||||
|
|
||||||
|
message RfidId {
|
||||||
|
uint32 value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncResponse {
|
||||||
|
int64 currentTime = 1;
|
||||||
|
bool pendingChanges = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SyncRequest {
|
||||||
|
optional int64 lastSync = 1;
|
||||||
|
map<uint64, uint32> accessLogs = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
�斉��3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"COXOg+uYMxAB"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
゚滑�3
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
import * as schema from '../src/schemas/database/schema';
|
||||||
|
import { hashPassword } from '../src/lib/utils/passwordHash';
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
config();
|
||||||
|
|
||||||
|
async function seedAdmin() {
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
if (!databaseUrl) {
|
||||||
|
throw new Error('DATABASE_URL environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('👤 Starting admin user seeding...');
|
||||||
|
|
||||||
|
const client = postgres(databaseUrl);
|
||||||
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create admin user
|
||||||
|
console.log('👤 Creating admin user...');
|
||||||
|
const adminPasswordHash = await hashPassword('admin123');
|
||||||
|
|
||||||
|
const adminUser = await db
|
||||||
|
.insert(schema.managementUsers)
|
||||||
|
.values({
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'User',
|
||||||
|
username: 'admin',
|
||||||
|
passwordHash: adminPasswordHash,
|
||||||
|
role: 'ADMIN',
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (adminUser.length > 0) {
|
||||||
|
console.log('✅ Admin user created with username: admin, password: admin123');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Admin user already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Admin user seeding completed successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during admin seeding:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedAdmin().catch((error) => {
|
||||||
|
console.error('❌ Admin seeding failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+243
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
|
import postgres from 'postgres';
|
||||||
|
import * as schema from '../src/schemas/database/schema';
|
||||||
|
import { hashPassword } from '../src/lib/utils/passwordHash';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
config();
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
if (!databaseUrl) {
|
||||||
|
throw new Error('DATABASE_URL environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🌱 Starting database seeding...');
|
||||||
|
|
||||||
|
const client = postgres(databaseUrl);
|
||||||
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create admin user
|
||||||
|
console.log('👤 Creating admin user...');
|
||||||
|
const adminPasswordHash = await hashPassword('admin123');
|
||||||
|
|
||||||
|
const adminUser = await db
|
||||||
|
.insert(schema.managementUsers)
|
||||||
|
.values({
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'User',
|
||||||
|
username: 'admin',
|
||||||
|
passwordHash: adminPasswordHash,
|
||||||
|
role: 'ADMIN',
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (adminUser.length > 0) {
|
||||||
|
console.log('✅ Admin user created with username: admin, password: admin123');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Admin user already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sample members
|
||||||
|
console.log('👥 Creating sample members...');
|
||||||
|
const members = [
|
||||||
|
{
|
||||||
|
firstName: 'Max',
|
||||||
|
lastName: 'Mustermann',
|
||||||
|
email: 'max.mustermann@example.com',
|
||||||
|
membershipNumber: 'MEM001',
|
||||||
|
phoneMobile: '+49123456789',
|
||||||
|
joinedAt: new Date('2024-01-15')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstName: 'Anna',
|
||||||
|
lastName: 'Schmidt',
|
||||||
|
email: 'anna.schmidt@example.com',
|
||||||
|
membershipNumber: 'MEM002',
|
||||||
|
phoneMobile: '+49987654321',
|
||||||
|
joinedAt: new Date('2024-02-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Müller',
|
||||||
|
email: 'peter.mueller@example.com',
|
||||||
|
membershipNumber: 'MEM003',
|
||||||
|
phoneMobile: '+49555666777',
|
||||||
|
joinedAt: new Date('2024-03-10')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const createdMembers = [];
|
||||||
|
for (const member of members) {
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.members)
|
||||||
|
.values(member)
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
createdMembers.push(result[0]);
|
||||||
|
console.log(`✅ Created member: ${member.firstName} ${member.lastName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sample RFID cards
|
||||||
|
console.log('🆔 Creating sample RFID cards...');
|
||||||
|
const rfidCards = [
|
||||||
|
{ rfidId: 'ABC123456789', status: 'NEW' as const },
|
||||||
|
{ rfidId: 'DEF987654321', status: 'NEW' as const },
|
||||||
|
{ rfidId: 'GHI456789123', status: 'ENGRAVED' as const },
|
||||||
|
{ rfidId: 'JKL789123456', status: 'NEW' as const },
|
||||||
|
{ rfidId: 'MNO321654987', status: 'NEW' as const }
|
||||||
|
];
|
||||||
|
|
||||||
|
const createdCards = [];
|
||||||
|
for (const card of rfidCards) {
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.rfidCards)
|
||||||
|
.values(card)
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
createdCards.push(result[0]);
|
||||||
|
console.log(`✅ Created RFID card: ${card.rfidId} (${card.status})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sample assignments
|
||||||
|
console.log('🔗 Creating sample card assignments...');
|
||||||
|
const assignments = [
|
||||||
|
{
|
||||||
|
memberId: createdMembers[0]?.id,
|
||||||
|
cardId: createdCards[0]?.id,
|
||||||
|
status: 'ASSIGNED' as const,
|
||||||
|
issuedAt: new Date('2024-01-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memberId: createdMembers[1]?.id,
|
||||||
|
cardId: createdCards[1]?.id,
|
||||||
|
status: 'ASSIGNED' as const,
|
||||||
|
issuedAt: new Date('2024-02-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
memberId: createdMembers[2]?.id,
|
||||||
|
cardId: createdCards[2]?.id,
|
||||||
|
status: 'ASSIGNED' as const,
|
||||||
|
issuedAt: new Date('2024-03-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
if (assignment.memberId && assignment.cardId) {
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.memberRfidCards)
|
||||||
|
.values(assignment)
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`✅ Assigned card ${createdCards.find((c) => c.id === assignment.cardId)?.rfidId} to ${createdMembers.find((m) => m.id === assignment.memberId)?.firstName} ${createdMembers.find((m) => m.id === assignment.memberId)?.lastName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sample devices
|
||||||
|
console.log('📱 Creating sample devices...');
|
||||||
|
const devices = [
|
||||||
|
{
|
||||||
|
name: 'Main Entrance Scanner',
|
||||||
|
apiKey: 'dev_' + uuidv4().replace(/-/g, ''),
|
||||||
|
type: 'RFID_SCANNER' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Office Door Lock',
|
||||||
|
apiKey: 'dev_' + uuidv4().replace(/-/g, ''),
|
||||||
|
type: 'LOCK_SYSTEM' as const
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const createdDevices = [];
|
||||||
|
for (const device of devices) {
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.devices)
|
||||||
|
.values(device)
|
||||||
|
.returning()
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
createdDevices.push(result[0]);
|
||||||
|
console.log(`✅ Created device: ${device.name} (${device.type})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all members and devices for access logs
|
||||||
|
const allMembers = await db.select().from(schema.members);
|
||||||
|
const allDevices = await db.select().from(schema.devices);
|
||||||
|
|
||||||
|
if (allMembers.length === 0 || allDevices.length === 0) {
|
||||||
|
console.log('⚠️ No members or devices found, skipping access log creation');
|
||||||
|
} else {
|
||||||
|
// Create sample access logs for the last 30 days
|
||||||
|
console.log('📊 Creating sample access logs...');
|
||||||
|
const accessLogs = [];
|
||||||
|
const now = new Date();
|
||||||
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
const date = new Date(thirtyDaysAgo.getTime() + i * 24 * 60 * 60 * 1000);
|
||||||
|
// Random number of accesses per day (5-50)
|
||||||
|
const accessesPerDay = Math.floor(Math.random() * 46) + 5;
|
||||||
|
|
||||||
|
for (let j = 0; j < accessesPerDay; j++) {
|
||||||
|
const randomMember = allMembers[Math.floor(Math.random() * allMembers.length)];
|
||||||
|
const randomDevice = allDevices[Math.floor(Math.random() * allDevices.length)];
|
||||||
|
const randomHour = Math.floor(Math.random() * 24);
|
||||||
|
const randomMinute = Math.floor(Math.random() * 60);
|
||||||
|
const accessedAt = new Date(date);
|
||||||
|
accessedAt.setHours(randomHour, randomMinute, 0, 0);
|
||||||
|
|
||||||
|
accessLogs.push({
|
||||||
|
memberId: randomMember.id,
|
||||||
|
deviceId: randomDevice.id,
|
||||||
|
accessedAt,
|
||||||
|
accessGranted: Math.random() > 0.05 // 95% success rate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const log of accessLogs) {
|
||||||
|
await db.insert(schema.accessLogs).values(log).onConflictDoNothing();
|
||||||
|
}
|
||||||
|
console.log(`✅ Created ${accessLogs.length} sample access logs over the last 30 days`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🎉 Database seeding completed successfully!');
|
||||||
|
console.log('\n📋 Summary:');
|
||||||
|
console.log('- Admin user: admin / admin123');
|
||||||
|
console.log('- Sample members, RFID cards, and assignments created');
|
||||||
|
console.log('- Sample devices created');
|
||||||
|
if (allMembers.length > 0 && allDevices.length > 0) {
|
||||||
|
console.log('- Sample access logs created for chart testing');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during seeding:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch((error) => {
|
||||||
|
console.error('❌ Seeding failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Moderne Basis mit den Original-Markenfarben */
|
||||||
|
--background: oklch(0.99 0.002 240);
|
||||||
|
--foreground: oklch(0.25 0.015 255);
|
||||||
|
|
||||||
|
/* Karten mit subtiler Erhebung */
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.25 0.015 255);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.25 0.015 255);
|
||||||
|
|
||||||
|
/* Primary = Modernisiertes #004A9B Blau */
|
||||||
|
--primary: oklch(0.45 0.14 255);
|
||||||
|
--primary-foreground: oklch(0.99 0.002 240);
|
||||||
|
|
||||||
|
/* Secondary = Aufgefrischtes Hellblau */
|
||||||
|
--secondary: oklch(0.97 0.015 240);
|
||||||
|
--secondary-foreground: oklch(0.25 0.015 255);
|
||||||
|
|
||||||
|
/* Muted = Subtiles Grau mit Blaustich */
|
||||||
|
--muted: oklch(0.96 0.008 240);
|
||||||
|
--muted-foreground: oklch(0.55 0.025 240);
|
||||||
|
|
||||||
|
/* Accent = Modernisiertes Türkis #006464 */
|
||||||
|
--accent: oklch(0.48 0.08 200);
|
||||||
|
--accent-foreground: oklch(0.99 0.002 240);
|
||||||
|
|
||||||
|
/* Destructive mit besserer Sichtbarkeit */
|
||||||
|
--destructive: oklch(0.58 0.22 25);
|
||||||
|
--destructive-foreground: oklch(0.99 0.002 240);
|
||||||
|
|
||||||
|
/* Borders & Inputs - dezenter */
|
||||||
|
--border: oklch(0.90 0.01 240);
|
||||||
|
--input: oklch(0.90 0.01 240);
|
||||||
|
--ring: oklch(0.45 0.14 255);
|
||||||
|
|
||||||
|
/* Charts mit harmonischer Palette */
|
||||||
|
--chart-1: oklch(0.45 0.14 255); /* Primary Blau */
|
||||||
|
--chart-2: oklch(0.48 0.08 200); /* Accent Türkis */
|
||||||
|
--chart-3: oklch(0.60 0.12 280); /* Violett */
|
||||||
|
--chart-4: oklch(0.65 0.15 150); /* Grün */
|
||||||
|
--chart-5: oklch(0.70 0.18 50); /* Orange */
|
||||||
|
|
||||||
|
/* Sidebar - aufgehelltes Design */
|
||||||
|
--sidebar: oklch(0.98 0.008 240);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.015 255);
|
||||||
|
--sidebar-primary: oklch(0.45 0.14 255);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.002 240);
|
||||||
|
--sidebar-accent: oklch(0.95 0.012 240);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.015 255);
|
||||||
|
--sidebar-border: oklch(0.92 0.01 240);
|
||||||
|
--sidebar-ring: oklch(0.45 0.14 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.12 0.012 255);
|
||||||
|
--foreground: oklch(0.92 0.01 240);
|
||||||
|
|
||||||
|
--card: oklch(0.16 0.012 255);
|
||||||
|
--card-foreground: oklch(0.92 0.01 240);
|
||||||
|
--popover: oklch(0.16 0.012 255);
|
||||||
|
--popover-foreground: oklch(0.92 0.01 240);
|
||||||
|
|
||||||
|
/* Primary - leuchtender im Dark Mode */
|
||||||
|
--primary: oklch(0.62 0.18 255);
|
||||||
|
--primary-foreground: oklch(0.12 0.012 255);
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
--secondary: oklch(0.20 0.015 240);
|
||||||
|
--secondary-foreground: oklch(0.92 0.01 240);
|
||||||
|
|
||||||
|
--muted: oklch(0.20 0.015 240);
|
||||||
|
--muted-foreground: oklch(0.68 0.03 240);
|
||||||
|
|
||||||
|
/* Accent - helleres Türkis */
|
||||||
|
--accent: oklch(0.58 0.10 190);
|
||||||
|
--accent-foreground: oklch(0.12 0.012 255);
|
||||||
|
|
||||||
|
--destructive: oklch(0.65 0.24 25);
|
||||||
|
--destructive-foreground: oklch(0.92 0.01 240);
|
||||||
|
|
||||||
|
--border: oklch(0.26 0.015 240);
|
||||||
|
--input: oklch(0.26 0.015 240);
|
||||||
|
--ring: oklch(0.62 0.18 255);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.62 0.18 255);
|
||||||
|
--chart-2: oklch(0.58 0.10 190);
|
||||||
|
--chart-3: oklch(0.65 0.14 280);
|
||||||
|
--chart-4: oklch(0.70 0.16 150);
|
||||||
|
--chart-5: oklch(0.75 0.20 50);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.14 0.012 255);
|
||||||
|
--sidebar-foreground: oklch(0.92 0.01 240);
|
||||||
|
--sidebar-primary: oklch(0.62 0.18 255);
|
||||||
|
--sidebar-primary-foreground: oklch(0.12 0.012 255);
|
||||||
|
--sidebar-accent: oklch(0.24 0.015 240);
|
||||||
|
--sidebar-accent-foreground: oklch(0.92 0.01 240);
|
||||||
|
--sidebar-border: oklch(0.26 0.015 240);
|
||||||
|
--sidebar-ring: oklch(0.62 0.18 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
interface Locals {
|
||||||
|
auth: {
|
||||||
|
admin?: import('$lib/server/auth/jwt').AdminTokenPayload;
|
||||||
|
member?: import('$lib/server/auth/jwt').MemberTokenPayload;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="%lang%">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.7.7
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: proto/control_communication.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
|
||||||
|
export const protobufPackage = "control_communication";
|
||||||
|
|
||||||
|
export interface RfidId {
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResponse {
|
||||||
|
currentTime: number;
|
||||||
|
pendingChanges: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncRequest {
|
||||||
|
lastSync?: number | undefined;
|
||||||
|
accessLogs: { [key: number]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncRequest_AccessLogsEntry {
|
||||||
|
key: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseRfidId(): RfidId {
|
||||||
|
return { value: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RfidId: MessageFns<RfidId> = {
|
||||||
|
encode(message: RfidId, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.value !== 0) {
|
||||||
|
writer.uint32(8).uint32(message.value);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): RfidId {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseRfidId();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = reader.uint32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): RfidId {
|
||||||
|
return { value: isSet(object.value) ? globalThis.Number(object.value) : 0 };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: RfidId): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.value !== 0) {
|
||||||
|
obj.value = Math.round(message.value);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<RfidId>, I>>(base?: I): RfidId {
|
||||||
|
return RfidId.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<RfidId>, I>>(object: I): RfidId {
|
||||||
|
const message = createBaseRfidId();
|
||||||
|
message.value = object.value ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseSyncResponse(): SyncResponse {
|
||||||
|
return { currentTime: 0, pendingChanges: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncResponse: MessageFns<SyncResponse> = {
|
||||||
|
encode(message: SyncResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.currentTime !== 0) {
|
||||||
|
writer.uint32(8).int64(message.currentTime);
|
||||||
|
}
|
||||||
|
if (message.pendingChanges !== false) {
|
||||||
|
writer.uint32(16).bool(message.pendingChanges);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): SyncResponse {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseSyncResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.currentTime = longToNumber(reader.int64());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.pendingChanges = reader.bool();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): SyncResponse {
|
||||||
|
return {
|
||||||
|
currentTime: isSet(object.currentTime) ? globalThis.Number(object.currentTime) : 0,
|
||||||
|
pendingChanges: isSet(object.pendingChanges) ? globalThis.Boolean(object.pendingChanges) : false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: SyncResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.currentTime !== 0) {
|
||||||
|
obj.currentTime = Math.round(message.currentTime);
|
||||||
|
}
|
||||||
|
if (message.pendingChanges !== false) {
|
||||||
|
obj.pendingChanges = message.pendingChanges;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<SyncResponse>, I>>(base?: I): SyncResponse {
|
||||||
|
return SyncResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<SyncResponse>, I>>(object: I): SyncResponse {
|
||||||
|
const message = createBaseSyncResponse();
|
||||||
|
message.currentTime = object.currentTime ?? 0;
|
||||||
|
message.pendingChanges = object.pendingChanges ?? false;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseSyncRequest(): SyncRequest {
|
||||||
|
return { lastSync: undefined, accessLogs: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncRequest: MessageFns<SyncRequest> = {
|
||||||
|
encode(message: SyncRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.lastSync !== undefined) {
|
||||||
|
writer.uint32(8).int64(message.lastSync);
|
||||||
|
}
|
||||||
|
Object.entries(message.accessLogs).forEach(([key, value]) => {
|
||||||
|
SyncRequest_AccessLogsEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join();
|
||||||
|
});
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): SyncRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseSyncRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.lastSync = longToNumber(reader.int64());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry2 = SyncRequest_AccessLogsEntry.decode(reader, reader.uint32());
|
||||||
|
if (entry2.value !== undefined) {
|
||||||
|
message.accessLogs[entry2.key] = entry2.value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): SyncRequest {
|
||||||
|
return {
|
||||||
|
lastSync: isSet(object.lastSync) ? globalThis.Number(object.lastSync) : undefined,
|
||||||
|
accessLogs: isObject(object.accessLogs)
|
||||||
|
? Object.entries(object.accessLogs).reduce<{ [key: number]: number }>((acc, [key, value]) => {
|
||||||
|
acc[globalThis.Number(key)] = Number(value);
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: SyncRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.lastSync !== undefined) {
|
||||||
|
obj.lastSync = Math.round(message.lastSync);
|
||||||
|
}
|
||||||
|
if (message.accessLogs) {
|
||||||
|
const entries = Object.entries(message.accessLogs);
|
||||||
|
if (entries.length > 0) {
|
||||||
|
obj.accessLogs = {};
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
obj.accessLogs[k] = Math.round(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<SyncRequest>, I>>(base?: I): SyncRequest {
|
||||||
|
return SyncRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<SyncRequest>, I>>(object: I): SyncRequest {
|
||||||
|
const message = createBaseSyncRequest();
|
||||||
|
message.lastSync = object.lastSync ?? undefined;
|
||||||
|
message.accessLogs = Object.entries(object.accessLogs ?? {}).reduce<{ [key: number]: number }>(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
acc[globalThis.Number(key)] = globalThis.Number(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseSyncRequest_AccessLogsEntry(): SyncRequest_AccessLogsEntry {
|
||||||
|
return { key: 0, value: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncRequest_AccessLogsEntry: MessageFns<SyncRequest_AccessLogsEntry> = {
|
||||||
|
encode(message: SyncRequest_AccessLogsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.key !== 0) {
|
||||||
|
writer.uint32(8).uint64(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== 0) {
|
||||||
|
writer.uint32(16).uint32(message.value);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): SyncRequest_AccessLogsEntry {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseSyncRequest_AccessLogsEntry();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.key = longToNumber(reader.uint64());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = reader.uint32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): SyncRequest_AccessLogsEntry {
|
||||||
|
return {
|
||||||
|
key: isSet(object.key) ? globalThis.Number(object.key) : 0,
|
||||||
|
value: isSet(object.value) ? globalThis.Number(object.value) : 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: SyncRequest_AccessLogsEntry): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.key !== 0) {
|
||||||
|
obj.key = Math.round(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== 0) {
|
||||||
|
obj.value = Math.round(message.value);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<SyncRequest_AccessLogsEntry>, I>>(base?: I): SyncRequest_AccessLogsEntry {
|
||||||
|
return SyncRequest_AccessLogsEntry.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<SyncRequest_AccessLogsEntry>, I>>(object: I): SyncRequest_AccessLogsEntry {
|
||||||
|
const message = createBaseSyncRequest_AccessLogsEntry();
|
||||||
|
message.key = object.key ?? 0;
|
||||||
|
message.value = object.value ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function longToNumber(int64: { toString(): string }): number {
|
||||||
|
const num = globalThis.Number(int64.toString());
|
||||||
|
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||||
|
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||||
|
}
|
||||||
|
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||||
|
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(value: any): boolean {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { configManager } from '$lib/config';
|
||||||
|
import jwtHelper from '$lib/server/auth/jwt';
|
||||||
|
import { handleAuth } from '$lib/server/auth/jwtSessionManager';
|
||||||
|
import type { Handle, ServerInit } from '@sveltejs/kit';
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
import { paraglideMiddleware } from '$paraglide/server';
|
||||||
|
import { setupDatabase } from '$lib/server/db/migrate';
|
||||||
|
|
||||||
|
let initPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
export const init: ServerInit = async () => {
|
||||||
|
await ensureInit();
|
||||||
|
configManager.loadConfig();
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
configManager.watchConfig();
|
||||||
|
}
|
||||||
|
await setupDatabase();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function ensureInit(): Promise<void> {
|
||||||
|
if (!initPromise) {
|
||||||
|
initPromise = jwtHelper
|
||||||
|
.init()
|
||||||
|
.then(() => {
|
||||||
|
/* initialized */
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// keep logging but rethrow so startup fails loudly if keys cannot be created
|
||||||
|
// caller can catch this if they want a degraded mode
|
||||||
|
console.error('jwtHelper.init() failed', err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paraglideHandle: Handle = ({ event, resolve }) =>
|
||||||
|
paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => {
|
||||||
|
event.request = localizedRequest;
|
||||||
|
return resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => {
|
||||||
|
return html.replace('%lang%', locale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const customLogHandle: Handle = async ({ resolve, event }) => {
|
||||||
|
console.log('Handling request for', event.url.pathname);
|
||||||
|
// delegate to the JWT session manager SvelteKit handle which sets event.locals.auth
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
export const handle: Handle = sequence(handleAuth, customLogHandle, paraglideHandle);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { deLocalizeUrl } from '$paraglide/runtime';
|
||||||
|
import type { Reroute } from '@sveltejs/kit';
|
||||||
|
import { initZodParaglide } from './schemas/zod_errors';
|
||||||
|
initZodParaglide();
|
||||||
|
|
||||||
|
export const reroute: Reroute = (request) => {
|
||||||
|
return deLocalizeUrl(request.url).pathname;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Select from '$lib/components/ui/select/index.js';
|
||||||
|
import { locales, getLocale, setLocale, type Locale } from '$paraglide/runtime';
|
||||||
|
import { Languages } from '@lucide/svelte';
|
||||||
|
let currentLocale = getLocale();
|
||||||
|
|
||||||
|
function switchLanguage(locale: string) {
|
||||||
|
setLocale(locale as (typeof locales)[number], {
|
||||||
|
//reload: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lables: Record<(typeof locales)[number], string> = {
|
||||||
|
en: 'English',
|
||||||
|
de: 'Deutsch'
|
||||||
|
};
|
||||||
|
const items = locales.map((locale) => ({
|
||||||
|
label: lables[locale],
|
||||||
|
value: locale
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root type="single" {items} value={currentLocale} onValueChange={switchLanguage}>
|
||||||
|
<Select.Trigger>
|
||||||
|
<Languages class="mr-2 h-4 w-4" />
|
||||||
|
<span>{lables[currentLocale]}</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each items as item}
|
||||||
|
<Select.Item value={item.value}>{item.label}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { theme } from '$lib/stores/theme.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { Sun, Moon } from '@lucide/svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onclick={theme.toggle}
|
||||||
|
class="h-9 w-9"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{#if $theme === 'dark'}
|
||||||
|
<Sun class="h-4 w-4" />
|
||||||
|
{:else}
|
||||||
|
<Moon class="h-4 w-4" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarHeader,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubItem
|
||||||
|
} from '$lib/components/ui/sidebar/index.js';
|
||||||
|
import { m } from '$paraglide/messages';
|
||||||
|
import type { ManagementUserPublic } from '$schemas/managementUserSchema';
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
ChevronRight,
|
||||||
|
Cpu,
|
||||||
|
CreditCard,
|
||||||
|
FileText,
|
||||||
|
LayoutDashboard,
|
||||||
|
LogOut,
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
UserCheck,
|
||||||
|
Users
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
|
||||||
|
let { user }: { user: ManagementUserPublic } = $props();
|
||||||
|
|
||||||
|
let expandedItems = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
|
function toggleExpanded(title: string) {
|
||||||
|
if (expandedItems.has(title)) {
|
||||||
|
expandedItems.delete(title);
|
||||||
|
} else {
|
||||||
|
expandedItems.add(title);
|
||||||
|
}
|
||||||
|
expandedItems = new Set(expandedItems); // Trigger reactivity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize expanded items based on current route
|
||||||
|
function initializeExpandedItems() {
|
||||||
|
const currentPath = page.url.pathname;
|
||||||
|
const initialExpanded = new Set<string>();
|
||||||
|
|
||||||
|
// Check navigation items with sub-items
|
||||||
|
for (const item of navigationItems) {
|
||||||
|
if (item.items) {
|
||||||
|
const hasActiveSubItem = item.items.some((subItem) => subItem.href === currentPath);
|
||||||
|
if (hasActiveSubItem) {
|
||||||
|
initialExpanded.add(item.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationItems: {
|
||||||
|
title: string;
|
||||||
|
icon: typeof BarChart3;
|
||||||
|
href?: string;
|
||||||
|
badge?: string;
|
||||||
|
items?: { title: string; href: string }[];
|
||||||
|
isActive?: boolean;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
title: m.sidebar_dashboard(),
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
href: '/admin',
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: m.sidebar_users(),
|
||||||
|
icon: Users,
|
||||||
|
href: '/admin/users'
|
||||||
|
// badge: '12'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: m.sidebar_members(),
|
||||||
|
icon: UserCheck,
|
||||||
|
items: [
|
||||||
|
{ title: m.sidebar_members(), href: '/admin/members' },
|
||||||
|
{ title: 'Import', href: '/admin/import' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: m.sidebar_rfid_cards(),
|
||||||
|
icon: CreditCard,
|
||||||
|
items: [
|
||||||
|
{ title: m.sidebar_rfid_cards(), href: '/admin/rfid-cards' },
|
||||||
|
{ title: m.sidebar_rfid_card_assignments(), href: '/admin/rfid-cards/assignments' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: m.sidebar_logs(),
|
||||||
|
icon: FileText,
|
||||||
|
items: [
|
||||||
|
{ title: m.sidebar_access_logs(), href: '/admin/access-logs' },
|
||||||
|
{ title: m.sidebar_system_logs(), href: '/admin/system-logs' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const settingsItems = [
|
||||||
|
{
|
||||||
|
title: m.sidebar_settings(),
|
||||||
|
icon: Settings,
|
||||||
|
href: '/admin/settings'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: m.sidebar_devices(),
|
||||||
|
icon: Cpu,
|
||||||
|
href: '/admin/devices'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize expanded items on mount
|
||||||
|
$effect(() => {
|
||||||
|
expandedItems = initializeExpandedItems();
|
||||||
|
});
|
||||||
|
|
||||||
|
function isActive(href: string | undefined): boolean {
|
||||||
|
return href != undefined && page.url.pathname === href;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar>
|
||||||
|
<SidebarHeader>
|
||||||
|
<div class="flex items-center gap-2 px-4 py-2">
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"
|
||||||
|
>
|
||||||
|
<LayoutDashboard class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-semibold">{m.sidebar_admin_panel()}</span>
|
||||||
|
<span class="truncate text-xs text-muted-foreground">{m.sidebar_management()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarHeader>
|
||||||
|
|
||||||
|
<SidebarContent>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>{m.sidebar_navigation()}</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{#each navigationItems as item}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
{#if item.items}
|
||||||
|
<SidebarMenuButton onclick={() => toggleExpanded(item.title)}>
|
||||||
|
<item.icon class="h-4 w-4" />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
<ChevronRight
|
||||||
|
class="ml-auto h-4 w-4 transition-transform {expandedItems.has(item.title)
|
||||||
|
? 'rotate-90'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
{#if expandedItems.has(item.title)}
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{#each item.items as subItem}
|
||||||
|
<SidebarMenuSubItem>
|
||||||
|
<a
|
||||||
|
href={subItem.href}
|
||||||
|
class="flex w-full items-center rounded-md px-4 py-2 text-sm hover:bg-accent {isActive(
|
||||||
|
subItem.href
|
||||||
|
)
|
||||||
|
? 'bg-accent'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
<span>{subItem.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class="flex w-full items-center gap-2 rounded-md px-4 py-2 text-sm hover:bg-accent {isActive(
|
||||||
|
item.href
|
||||||
|
)
|
||||||
|
? 'bg-accent'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
<item.icon class="h-4 w-4" />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
{#if item.badge}
|
||||||
|
<Badge variant="secondary" class="ml-auto h-5 text-xs">
|
||||||
|
{item.badge}
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
{/each}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>{m.sidebar_system()}</SidebarGroupLabel>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{#each settingsItems as item}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class="flex w-full items-center gap-2 rounded-md px-4 py-2 text-sm hover:bg-accent {isActive(
|
||||||
|
item.href
|
||||||
|
)
|
||||||
|
? 'bg-accent'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
<item.icon class="h-4 w-4" />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
{/each}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarFooter>
|
||||||
|
<div class="px-4 py-2">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<User class="h-4 w-4" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium">{user.firstName} {user.lastName}</div>
|
||||||
|
<div class="text-xs text-muted-foreground">{user.username}</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onclick={() => (window.location.href = '/admin/logout')}>
|
||||||
|
<LogOut class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarFooter>
|
||||||
|
</Sidebar>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as AppSidebar } from './app-sidebar.svelte';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ThemeToggle } from './ThemeToggle.svelte';
|
||||||
|
export { default as LanguageSwitcher } from './LanguageSwitcher.svelte';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.ActionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</AlertDialogPrimitive.Action>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.CancelProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
bind:ref
|
||||||
|
class={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</AlertDialogPrimitive.Cancel>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import AlertDialogOverlay from './alert-dialog-overlay.svelte';
|
||||||
|
import { cn, type WithoutChild, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Portal {...portalProps}>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
class={cn(
|
||||||
|
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</AlertDialogPrimitive.Portal>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
class={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
class={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
class={cn(
|
||||||
|
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
class={cn('text-lg font-semibold', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||||
|
import Trigger from './alert-dialog-trigger.svelte';
|
||||||
|
import Title from './alert-dialog-title.svelte';
|
||||||
|
import Action from './alert-dialog-action.svelte';
|
||||||
|
import Cancel from './alert-dialog-cancel.svelte';
|
||||||
|
import Footer from './alert-dialog-footer.svelte';
|
||||||
|
import Header from './alert-dialog-header.svelte';
|
||||||
|
import Overlay from './alert-dialog-overlay.svelte';
|
||||||
|
import Content from './alert-dialog-content.svelte';
|
||||||
|
import Description from './alert-dialog-description.svelte';
|
||||||
|
|
||||||
|
const Root = AlertDialogPrimitive.Root;
|
||||||
|
const Portal = AlertDialogPrimitive.Portal;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
Cancel,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
//
|
||||||
|
Root as AlertDialog,
|
||||||
|
Title as AlertDialogTitle,
|
||||||
|
Action as AlertDialogAction,
|
||||||
|
Cancel as AlertDialogCancel,
|
||||||
|
Portal as AlertDialogPortal,
|
||||||
|
Footer as AlertDialogFooter,
|
||||||
|
Header as AlertDialogHeader,
|
||||||
|
Trigger as AlertDialogTrigger,
|
||||||
|
Overlay as AlertDialogOverlay,
|
||||||
|
Content as AlertDialogContent,
|
||||||
|
Description as AlertDialogDescription
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-description"
|
||||||
|
class={cn(
|
||||||
|
'col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert-title"
|
||||||
|
class={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const alertVariants = tv({
|
||||||
|
base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-card text-card-foreground',
|
||||||
|
destructive:
|
||||||
|
'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AlertVariant = VariantProps<typeof alertVariants>['variant'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
variant?: AlertVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="alert"
|
||||||
|
class={cn(alertVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Root from './alert.svelte';
|
||||||
|
import Description from './alert-description.svelte';
|
||||||
|
import Title from './alert-title.svelte';
|
||||||
|
export { alertVariants, type AlertVariant } from './alert.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Description,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Alert,
|
||||||
|
Description as AlertDescription,
|
||||||
|
Title as AlertTitle
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.FallbackProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
class={cn('flex size-full items-center justify-center rounded-full bg-muted', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.ImageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
bind:ref
|
||||||
|
data-slot="avatar-image"
|
||||||
|
class={cn('aspect-square size-full', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Avatar as AvatarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
loadingStatus = $bindable('loading'),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AvatarPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:loadingStatus
|
||||||
|
data-slot="avatar"
|
||||||
|
class={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Root from './avatar.svelte';
|
||||||
|
import Image from './avatar-image.svelte';
|
||||||
|
import Fallback from './avatar-fallback.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Image,
|
||||||
|
Fallback,
|
||||||
|
//
|
||||||
|
Root as Avatar,
|
||||||
|
Image as AvatarImage,
|
||||||
|
Fallback as AvatarFallback
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
|
||||||
|
outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>['variant'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? 'a' : 'span'}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from './badge.svelte';
|
||||||
|
export { badgeVariants, type BadgeVariant } from './badge.svelte';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
class={cn('flex size-9 items-center justify-center', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<EllipsisIcon class="size-4" />
|
||||||
|
<span class="sr-only">More</span>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLLiAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
class={cn('inline-flex items-center gap-1.5', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
href = undefined,
|
||||||
|
child,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
child?: Snippet<[{ props: HTMLAnchorAttributes }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const attrs = $derived({
|
||||||
|
'data-slot': 'breadcrumb-link',
|
||||||
|
class: cn('hover:text-foreground transition-colors', className),
|
||||||
|
href,
|
||||||
|
...restProps
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if child}
|
||||||
|
{@render child({ props: attrs })}
|
||||||
|
{:else}
|
||||||
|
<a bind:this={ref} {...attrs}>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLOlAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLOlAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ol
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
class={cn(
|
||||||
|
'flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
class={cn('font-normal text-foreground', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLLiAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
class={cn('[&>svg]:size-3.5', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if children}
|
||||||
|
{@render children?.()}
|
||||||
|
{:else}
|
||||||
|
<ChevronRightIcon />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="breadcrumb"
|
||||||
|
class={className}
|
||||||
|
aria-label="breadcrumb"
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from './breadcrumb.svelte';
|
||||||
|
import Ellipsis from './breadcrumb-ellipsis.svelte';
|
||||||
|
import Item from './breadcrumb-item.svelte';
|
||||||
|
import Separator from './breadcrumb-separator.svelte';
|
||||||
|
import Link from './breadcrumb-link.svelte';
|
||||||
|
import List from './breadcrumb-list.svelte';
|
||||||
|
import Page from './breadcrumb-page.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Ellipsis,
|
||||||
|
Item,
|
||||||
|
Separator,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
Page,
|
||||||
|
//
|
||||||
|
Root as Breadcrumb,
|
||||||
|
Ellipsis as BreadcrumbEllipsis,
|
||||||
|
Item as BreadcrumbItem,
|
||||||
|
Separator as BreadcrumbSeparator,
|
||||||
|
Link as BreadcrumbLink,
|
||||||
|
List as BreadcrumbList,
|
||||||
|
Page as BreadcrumbPage
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||||
|
outline:
|
||||||
|
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'size-9'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = 'button',
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? 'link' : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants
|
||||||
|
} from './button.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import type Calendar from './calendar.svelte';
|
||||||
|
import CalendarMonthSelect from './calendar-month-select.svelte';
|
||||||
|
import CalendarYearSelect from './calendar-year-select.svelte';
|
||||||
|
import { DateFormatter, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||||
|
|
||||||
|
let {
|
||||||
|
captionLayout,
|
||||||
|
months,
|
||||||
|
monthFormat,
|
||||||
|
years,
|
||||||
|
yearFormat,
|
||||||
|
month,
|
||||||
|
locale,
|
||||||
|
placeholder = $bindable(),
|
||||||
|
monthIndex = 0
|
||||||
|
}: {
|
||||||
|
captionLayout: ComponentProps<typeof Calendar>['captionLayout'];
|
||||||
|
months: ComponentProps<typeof CalendarMonthSelect>['months'];
|
||||||
|
monthFormat: ComponentProps<typeof CalendarMonthSelect>['monthFormat'];
|
||||||
|
years: ComponentProps<typeof CalendarYearSelect>['years'];
|
||||||
|
yearFormat: ComponentProps<typeof CalendarYearSelect>['yearFormat'];
|
||||||
|
month: DateValue;
|
||||||
|
placeholder: DateValue | undefined;
|
||||||
|
locale: string;
|
||||||
|
monthIndex: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function formatYear(date: DateValue) {
|
||||||
|
const dateObj = date.toDate(getLocalTimeZone());
|
||||||
|
if (typeof yearFormat === 'function') return yearFormat(dateObj.getFullYear());
|
||||||
|
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMonth(date: DateValue) {
|
||||||
|
const dateObj = date.toDate(getLocalTimeZone());
|
||||||
|
if (typeof monthFormat === 'function') return monthFormat(dateObj.getMonth() + 1);
|
||||||
|
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet MonthSelect()}
|
||||||
|
<CalendarMonthSelect
|
||||||
|
{months}
|
||||||
|
{monthFormat}
|
||||||
|
value={month.month}
|
||||||
|
onchange={(e) => {
|
||||||
|
if (!placeholder) return;
|
||||||
|
const v = Number.parseInt(e.currentTarget.value);
|
||||||
|
const newPlaceholder = placeholder.set({ month: v });
|
||||||
|
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet YearSelect()}
|
||||||
|
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#if captionLayout === 'dropdown'}
|
||||||
|
{@render MonthSelect()}
|
||||||
|
{@render YearSelect()}
|
||||||
|
{:else if captionLayout === 'dropdown-months'}
|
||||||
|
{@render MonthSelect()}
|
||||||
|
{#if placeholder}
|
||||||
|
{formatYear(placeholder)}
|
||||||
|
{/if}
|
||||||
|
{:else if captionLayout === 'dropdown-years'}
|
||||||
|
{#if placeholder}
|
||||||
|
{formatMonth(placeholder)}
|
||||||
|
{/if}
|
||||||
|
{@render YearSelect()}
|
||||||
|
{:else}
|
||||||
|
{formatMonth(month)} {formatYear(month)}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.CellProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Cell
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
'relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-l-md [&:last-child[data-selected]_[data-bits-day]]:rounded-r-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.DayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Day
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant: 'ghost' }),
|
||||||
|
'flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none',
|
||||||
|
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground',
|
||||||
|
'data-[selected]:bg-primary data-[selected]:text-primary-foreground dark:data-[selected]:hover:bg-accent/50',
|
||||||
|
// Outside months
|
||||||
|
'[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground',
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||||
|
// Unavailable
|
||||||
|
'data-[unavailable]:text-muted-foreground data-[unavailable]:line-through',
|
||||||
|
// hover
|
||||||
|
'dark:hover:text-accent-foreground',
|
||||||
|
// focus
|
||||||
|
'focus:relative focus:border-ring focus:ring-ring/50',
|
||||||
|
// inner spans
|
||||||
|
'[&>span]:text-xs [&>span]:opacity-70',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridBodyProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridHeadProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridRowProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.GridRow bind:ref class={cn('flex', className)} {...restProps} />
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.GridProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Grid
|
||||||
|
bind:ref
|
||||||
|
class={cn('mt-4 flex w-full border-collapse flex-col gap-1', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeadCellProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.HeadCell
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
'w-(--cell-size) rounded-md text-[0.8rem] font-normal text-muted-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeaderProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Header
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.HeadingProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CalendarPrimitive.Heading
|
||||||
|
bind:ref
|
||||||
|
class={cn('px-(--cell-size) text-sm font-medium', className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
onchange,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'relative flex rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||||
|
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||||
|
<select {...props} {value} {onchange}>
|
||||||
|
{#each monthItems as monthItem (monthItem.value)}
|
||||||
|
<option
|
||||||
|
value={monthItem.value}
|
||||||
|
selected={value !== undefined
|
||||||
|
? monthItem.value === value
|
||||||
|
: monthItem.value === selectedMonthItem.value}
|
||||||
|
>
|
||||||
|
{monthItem.label}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span
|
||||||
|
class="flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm font-medium select-none [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.MonthSelect>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type WithElementRef, cn } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...restProps} bind:this={ref} class={cn('flex flex-col', className)}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn('relative flex flex-col gap-4 md:flex-row', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
{...restProps}
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn('absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', className)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||||
|
import { buttonVariants, type ButtonVariant } from '$lib/components/ui/button/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
variant = 'ghost',
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.NextButtonProps & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fallback()}
|
||||||
|
<ChevronRightIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<CalendarPrimitive.NextButton
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant }),
|
||||||
|
'size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
children={children || Fallback}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||||
|
import { buttonVariants, type ButtonVariant } from '$lib/components/ui/button/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
variant = 'ghost',
|
||||||
|
...restProps
|
||||||
|
}: CalendarPrimitive.PrevButtonProps & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet Fallback()}
|
||||||
|
<ChevronLeftIcon class="size-4" />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<CalendarPrimitive.PrevButton
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
buttonVariants({ variant }),
|
||||||
|
'size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
children={children || Fallback}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||||
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'relative flex rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||||
|
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||||
|
<select {...props} {value}>
|
||||||
|
{#each yearItems as yearItem (yearItem.value)}
|
||||||
|
<option
|
||||||
|
value={yearItem.value}
|
||||||
|
selected={value !== undefined
|
||||||
|
? yearItem.value === value
|
||||||
|
: yearItem.value === selectedYearItem.value}
|
||||||
|
>
|
||||||
|
{yearItem.label}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span
|
||||||
|
class="flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm font-medium select-none [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.YearSelect>
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive } from 'bits-ui';
|
||||||
|
import * as Calendar from './index.js';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||||
|
import type { ButtonVariant } from '../button/button.svelte';
|
||||||
|
import { isEqualMonth, type DateValue } from '@internationalized/date';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
placeholder = $bindable(),
|
||||||
|
class: className,
|
||||||
|
weekdayFormat = 'short',
|
||||||
|
buttonVariant = 'ghost',
|
||||||
|
captionLayout = 'label',
|
||||||
|
locale = 'en-US',
|
||||||
|
months: monthsProp,
|
||||||
|
years,
|
||||||
|
monthFormat: monthFormatProp,
|
||||||
|
yearFormat = 'numeric',
|
||||||
|
day,
|
||||||
|
disableDaysOutsideMonth = false,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
|
||||||
|
buttonVariant?: ButtonVariant;
|
||||||
|
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
|
||||||
|
months?: CalendarPrimitive.MonthSelectProps['months'];
|
||||||
|
years?: CalendarPrimitive.YearSelectProps['years'];
|
||||||
|
monthFormat?: CalendarPrimitive.MonthSelectProps['monthFormat'];
|
||||||
|
yearFormat?: CalendarPrimitive.YearSelectProps['yearFormat'];
|
||||||
|
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const monthFormat = $derived.by(() => {
|
||||||
|
if (monthFormatProp) return monthFormatProp;
|
||||||
|
if (captionLayout.startsWith('dropdown')) return 'short';
|
||||||
|
return 'long';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Discriminated Unions + Destructing (required for bindable) do not
|
||||||
|
get along, so we shut typescript up by casting `value` to `never`.
|
||||||
|
-->
|
||||||
|
<CalendarPrimitive.Root
|
||||||
|
bind:value={value as never}
|
||||||
|
bind:ref
|
||||||
|
bind:placeholder
|
||||||
|
{weekdayFormat}
|
||||||
|
{disableDaysOutsideMonth}
|
||||||
|
class={cn(
|
||||||
|
'group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{locale}
|
||||||
|
{monthFormat}
|
||||||
|
{yearFormat}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ months, weekdays })}
|
||||||
|
<Calendar.Months>
|
||||||
|
<Calendar.Nav>
|
||||||
|
<Calendar.PrevButton variant={buttonVariant} />
|
||||||
|
<Calendar.NextButton variant={buttonVariant} />
|
||||||
|
</Calendar.Nav>
|
||||||
|
{#each months as month, monthIndex (month)}
|
||||||
|
<Calendar.Month>
|
||||||
|
<Calendar.Header>
|
||||||
|
<Calendar.Caption
|
||||||
|
{captionLayout}
|
||||||
|
months={monthsProp}
|
||||||
|
{monthFormat}
|
||||||
|
{years}
|
||||||
|
{yearFormat}
|
||||||
|
month={month.value}
|
||||||
|
bind:placeholder
|
||||||
|
{locale}
|
||||||
|
{monthIndex}
|
||||||
|
/>
|
||||||
|
</Calendar.Header>
|
||||||
|
<Calendar.Grid>
|
||||||
|
<Calendar.GridHead>
|
||||||
|
<Calendar.GridRow class="select-none">
|
||||||
|
{#each weekdays as weekday (weekday)}
|
||||||
|
<Calendar.HeadCell>
|
||||||
|
{weekday.slice(0, 2)}
|
||||||
|
</Calendar.HeadCell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
</Calendar.GridHead>
|
||||||
|
<Calendar.GridBody>
|
||||||
|
{#each month.weeks as weekDates (weekDates)}
|
||||||
|
<Calendar.GridRow class="mt-2 w-full">
|
||||||
|
{#each weekDates as date (date)}
|
||||||
|
<Calendar.Cell {date} month={month.value}>
|
||||||
|
{#if day}
|
||||||
|
{@render day({
|
||||||
|
day: date,
|
||||||
|
outsideMonth: !isEqualMonth(date, month.value)
|
||||||
|
})}
|
||||||
|
{:else}
|
||||||
|
<Calendar.Day />
|
||||||
|
{/if}
|
||||||
|
</Calendar.Cell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridBody>
|
||||||
|
</Calendar.Grid>
|
||||||
|
</Calendar.Month>
|
||||||
|
{/each}
|
||||||
|
</Calendar.Months>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.Root>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Root from './calendar.svelte';
|
||||||
|
import Cell from './calendar-cell.svelte';
|
||||||
|
import Day from './calendar-day.svelte';
|
||||||
|
import Grid from './calendar-grid.svelte';
|
||||||
|
import Header from './calendar-header.svelte';
|
||||||
|
import Months from './calendar-months.svelte';
|
||||||
|
import GridRow from './calendar-grid-row.svelte';
|
||||||
|
import Heading from './calendar-heading.svelte';
|
||||||
|
import GridBody from './calendar-grid-body.svelte';
|
||||||
|
import GridHead from './calendar-grid-head.svelte';
|
||||||
|
import HeadCell from './calendar-head-cell.svelte';
|
||||||
|
import NextButton from './calendar-next-button.svelte';
|
||||||
|
import PrevButton from './calendar-prev-button.svelte';
|
||||||
|
import MonthSelect from './calendar-month-select.svelte';
|
||||||
|
import YearSelect from './calendar-year-select.svelte';
|
||||||
|
import Month from './calendar-month.svelte';
|
||||||
|
import Nav from './calendar-nav.svelte';
|
||||||
|
import Caption from './calendar-caption.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Day,
|
||||||
|
Cell,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Months,
|
||||||
|
GridRow,
|
||||||
|
Heading,
|
||||||
|
GridBody,
|
||||||
|
GridHead,
|
||||||
|
HeadCell,
|
||||||
|
NextButton,
|
||||||
|
PrevButton,
|
||||||
|
Nav,
|
||||||
|
Month,
|
||||||
|
YearSelect,
|
||||||
|
MonthSelect,
|
||||||
|
Caption,
|
||||||
|
//
|
||||||
|
Root as Calendar
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn('px-6', className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user