Initial commit

This commit is contained in:
2025-10-06 18:11:03 +02:00
commit 93cd6872a9
404 changed files with 35324 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.env
.env.example
+14
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+10
View File
@@ -0,0 +1,10 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
/drizzle/
+16
View File
@@ -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"
}
+15
View File
@@ -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", "/"]
}
}
}
+215
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["inlang.vs-code-extension"]
}
+44
View File
@@ -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"]
+163
View File
@@ -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]
+16
View File
@@ -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"
}
+41
View File
@@ -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
View File
@@ -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
+23
View File
@@ -0,0 +1,23 @@
{
"app": {
"name": "My SvelteKit1323",
"version": "1.0.013",
"supportEmail": "support@example.com",
"footerText": "&copy; 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"]
}
}
}
+28
View File
@@ -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-----
+9
View File
@@ -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-----
+13
View File
@@ -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
});
+121
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1758824141426,
"tag": "0000_peaceful_susan_delgado",
"breakpoints": true
}
]
}
+62
View File
@@ -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
}
}
}
);
+545
View File
@@ -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"
}
}
+546
View File
@@ -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"
}
}
+8458
View File
File diff suppressed because it is too large Load Diff
+91
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
cache
+1
View File
@@ -0,0 +1 @@
8jHSxX0rZat6wCtV9T
+12
View File
@@ -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"
}
}
+16
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
3
+1
View File
@@ -0,0 +1 @@
"COXOg+uYMxAB"
+1
View File
@@ -0,0 +1 @@
゚滑3
+59
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+18
View File
@@ -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 {};
+11
View File
@@ -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;
}
+52
View File
@@ -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);
+8
View File
@@ -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;
};
+1
View File
@@ -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>
+19
View File
@@ -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';
+2
View File
@@ -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>
+44
View File
@@ -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>
+14
View File
@@ -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}
/>
+13
View File
@@ -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
};
+49
View File
@@ -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>
+2
View File
@@ -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>
+25
View File
@@ -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}
+17
View File
@@ -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>
+40
View File
@@ -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