Compare commits
167 Commits
66ce257198
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d25fa005c | ||
|
|
20a693a80c | ||
|
|
242d37713d | ||
|
|
4b83be6ed8 | ||
|
|
8fcb73f24d | ||
|
|
11740cb503 | ||
|
|
06f8078b17 | ||
|
|
18f5dadb06 | ||
|
|
87f08dd3be | ||
|
|
2af4575ff2 | ||
|
|
560623788a | ||
|
|
5d55f5a8d9 | ||
|
|
28dfe7ecde | ||
|
|
741145b960 | ||
|
|
490b29295f | ||
|
|
ded5e6ceb1 | ||
|
|
b81804439e | ||
|
|
9a9af9f002 | ||
|
|
cd495584b0 | ||
|
|
bbead3c43b | ||
|
|
ce18324391 | ||
|
|
c9d5a88dbf | ||
|
|
380fee09c1 | ||
|
|
e35524132e | ||
|
|
af000aa4bc | ||
|
|
90ed0925ca | ||
|
|
7c0a6fedb5 | ||
|
|
0e6edc8e65 | ||
|
|
45a219625a | ||
|
|
ee10389f1d | ||
|
|
073d353764 | ||
|
|
9d2b33f832 | ||
|
|
e60aaa1d69 | ||
|
|
ca99e28433 | ||
|
|
9427492cb1 | ||
|
|
60d3f075bf | ||
|
|
d473aef3a9 | ||
|
|
ef4d3c9576 | ||
|
|
9a8b386931 | ||
|
|
9c429185dc | ||
|
|
c7865d0582 | ||
|
|
feb8abcc42 | ||
|
|
c8d0904fd7 | ||
|
|
294ad76e4b | ||
|
|
ca441d51e7 | ||
|
|
39c060794a | ||
|
|
c6ea179eca | ||
|
|
0d6013d566 | ||
|
|
0c3204df15 | ||
|
|
cfc10ab087 | ||
|
|
df6125b7cb | ||
|
|
7af66ee9de | ||
|
|
b2b702c21d | ||
|
|
fa996692fe | ||
|
|
8258a7c2a3 | ||
|
|
37ccbaaba4 | ||
|
|
c810e48451 | ||
|
|
8f737282f2 | ||
|
|
3756205ad4 | ||
|
|
68851c6257 | ||
|
|
8d56a9ad48 | ||
|
|
3d349a709c | ||
|
|
d1d5d839ae | ||
|
|
8ec9fb247f | ||
|
|
f5df70fba8 | ||
|
|
60a12c97be | ||
|
|
bb56d1f7c7 | ||
|
|
f719a0bbf5 | ||
|
|
05d94ae09c | ||
|
|
ff3106b8be | ||
|
|
6937ab333c | ||
|
|
1f61c9ad71 | ||
|
|
29f405385e | ||
|
|
298ef9843e | ||
|
|
aa1bd00e80 | ||
|
|
ac0b7234d4 | ||
|
|
c28354ed2d | ||
|
|
2342ce24de | ||
|
|
c6be9d2302 | ||
|
|
f00e0fa758 | ||
|
|
1d57b8e8e8 | ||
|
|
7216a48f4e | ||
|
|
f4e57e7558 | ||
|
|
770ef34c22 | ||
|
|
64310282ca | ||
|
|
91787b616e | ||
|
|
309e3a9d1e | ||
|
|
903cd6df28 | ||
|
|
0ba938be21 | ||
|
|
ef98745732 | ||
|
|
e3ebbe596c | ||
|
|
658cc9aecd | ||
|
|
a2e8abbf6b | ||
|
|
b0271f8443 | ||
|
|
e553c2dc2e | ||
|
|
20754b4422 | ||
|
|
386b50e857 | ||
|
|
34cf3a1e33 | ||
|
|
d9605fde58 | ||
|
|
9c9430ca9c | ||
|
|
2ffd1f439f | ||
|
|
ad599ae3f4 | ||
|
|
8137f121ed | ||
|
|
82558edd5a | ||
|
|
421b4753e5 | ||
|
|
e0717ec09a | ||
|
|
d355c6906e | ||
|
|
c42adc858f | ||
|
|
7c01b77445 | ||
|
|
a2886fc1e0 | ||
|
|
6b408d64a7 | ||
|
|
3eeb35c768 | ||
|
|
b7682f8dc3 | ||
|
|
dde3b3d47b | ||
|
|
c607622185 | ||
|
|
2866917aef | ||
|
|
f55ef5cf70 | ||
|
|
577e0fe2f7 | ||
|
|
c23a6a6b5f | ||
|
|
c34f9c28a5 | ||
|
|
3493e83e84 | ||
|
|
03b3683b63 | ||
|
|
d5a8b16e43 | ||
|
|
48e21736ea | ||
|
|
ab168311a9 | ||
|
|
54faee731d | ||
|
|
4a539638f8 | ||
|
|
09a0c9bba9 | ||
|
|
9472577d5e | ||
|
|
f180f59546 | ||
|
|
0e12286f15 | ||
|
|
cf037db080 | ||
|
|
012a57956a | ||
|
|
6c18accae4 | ||
|
|
2b500ca187 | ||
|
|
afe0a0de54 | ||
|
|
3b08e49d6f | ||
|
|
d688101378 | ||
|
|
e9d6b58f20 | ||
|
|
42edc70490 | ||
|
|
64b368e617 | ||
|
|
e11a05a85f | ||
|
|
89841ade55 | ||
|
|
89a7780c54 | ||
|
|
9d83afa525 | ||
|
|
d1273d3e23 | ||
|
|
0fab8791f9 | ||
|
|
59d9169646 | ||
|
|
f1fe64855d | ||
|
|
743493517b | ||
|
|
8787c8c2c1 | ||
|
|
993d7920d4 | ||
|
|
861d029ce5 | ||
|
|
2fdb484451 | ||
|
|
2492f410b1 | ||
|
|
447f149423 | ||
|
|
a8bc049af7 | ||
|
|
c34c46cbc2 | ||
|
|
32a473fe29 | ||
|
|
cce2866b52 | ||
|
|
3ae1ffd403 | ||
|
|
77619c42bd | ||
|
|
67ef3a2fca | ||
|
|
c2d5188765 | ||
|
|
f68ca9abc5 | ||
|
|
183e4da7f4 | ||
|
|
11c55a17ea |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,7 +14,6 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
@@ -40,14 +39,13 @@ go.work
|
||||
!*.css
|
||||
!go.sum
|
||||
!go.mod
|
||||
!*.sql
|
||||
|
||||
#!*.sql
|
||||
!README.md
|
||||
!LICENSE
|
||||
|
||||
# all template files:
|
||||
!*.template*
|
||||
|
||||
!frontend/*
|
||||
# Docker stuff
|
||||
!compose.yml
|
||||
!Dockerfile
|
||||
|
||||
10
compose.yml
10
compose.yml
@@ -1,10 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: carsharingBackend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./configs/config.json:/root/configs/config.json:ro
|
||||
- ./data/db.sqlite3:/root/data/db.sqlite3
|
||||
- ./templates:/root/templates:ro
|
||||
37
config.json.template
Normal file
37
config.json.template
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"db": {
|
||||
"Path": "data/db.sqlite3"
|
||||
},
|
||||
"site": {
|
||||
"AllowOrigins": "http://localhost:5173,http://localhost:3000",
|
||||
"WebsiteTitle": "GoMembership",
|
||||
"BaseUrl": "http://localhost:3000"
|
||||
},
|
||||
"auth": {
|
||||
"APIKey": "CHANGE_THIS"
|
||||
},
|
||||
"smtp": {
|
||||
"Host": "smtp.example.com",
|
||||
"User": "user@example.com",
|
||||
"Password": "CHANGE_THIS",
|
||||
"Port": 465
|
||||
},
|
||||
"templates": {
|
||||
"MailPath": "templates/email",
|
||||
"HTMLPath": "templates/html",
|
||||
"StaticPath": "templates/css",
|
||||
"LogoURI": "/static/logo.png"
|
||||
},
|
||||
"recipients": {
|
||||
"ContactForm": "contact@example.com",
|
||||
"UserRegistration": "admin@example.com",
|
||||
"AdminEmail": "admin@example.com"
|
||||
},
|
||||
"security": {
|
||||
"RateLimits": {
|
||||
"Limit": 1,
|
||||
"Burst": 60
|
||||
}
|
||||
},
|
||||
"Environment": "development"
|
||||
}
|
||||
2
frontend/.env.template
Normal file
2
frontend/.env.template
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_BASE_API_URI_DEV=http://127.0.0.1:8080/api
|
||||
VITE_BASE_API_URI_PROD=
|
||||
39
frontend/.gitignore
vendored
Normal file
39
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
test-results
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.template
|
||||
!.env.test
|
||||
|
||||
!.npmrc # NPM configuration
|
||||
!.prettierrc # Prettier configuration
|
||||
!.prettierignore # Prettier ignore rules
|
||||
!eslint.config.js # ESLint configuration
|
||||
!jsconfig.json # JavaScript configuration
|
||||
!package.json # Project dependencies and scripts
|
||||
!package-lock.json # Lock file for exact dependency versions
|
||||
!playwright.config.js # Playwright test configuration
|
||||
!README.md # Project documentation
|
||||
!svelte.config.js # Svelte configuration
|
||||
|
||||
# Vite
|
||||
!vite.config.js # Vite configuration
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
!src/**
|
||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
4
frontend/.prettierignore
Normal file
4
frontend/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
15
frontend/.prettierrc
Normal file
15
frontend/.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:23-alpine AS deploy-node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "build/index.js"]
|
||||
@@ -1,6 +1,20 @@
|
||||
# Frontend
|
||||
# sv
|
||||
|
||||
## Run locally
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
@@ -10,3 +24,15 @@ npm run dev
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
|
||||
24
frontend/eslint.config.js
Normal file
24
frontend/eslint.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
19
frontend/jsconfig.json
Normal file
19
frontend/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
4278
frontend/package-lock.json
generated
Normal file
4278
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
frontend/package.json
Normal file
42
frontend/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "frontend.new",
|
||||
"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 ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run && npm run test:e2e",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte-i18n": "^4.0.1"
|
||||
}
|
||||
}
|
||||
10
frontend/playwright.config.js
Normal file
10
frontend/playwright.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
|
||||
testDir: 'e2e'
|
||||
});
|
||||
173
frontend/src/app.d.ts
vendored
173
frontend/src/app.d.ts
vendored
@@ -1,84 +1,137 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
|
||||
interface Subscription {
|
||||
id: number | -1;
|
||||
name: string | "";
|
||||
details?: string | "";
|
||||
conditions?: string | "";
|
||||
monthly_fee?: number | -1;
|
||||
hourly_rate?: number | -1;
|
||||
included_hours_per_year?: number | 0;
|
||||
included_hours_per_month?: number | 0;
|
||||
id: number | -1;
|
||||
name: string | '';
|
||||
details: string | '';
|
||||
conditions: string | '';
|
||||
monthly_fee: number | 0;
|
||||
hourly_rate: number | 0;
|
||||
included_hours_per_year: number | 0;
|
||||
included_hours_per_month: number | 0;
|
||||
}
|
||||
|
||||
interface Membership {
|
||||
id: number | -1;
|
||||
status: number | -1;
|
||||
start_date: string | "";
|
||||
end_date: string | "";
|
||||
parent_member_id: number | -1;
|
||||
subscription_model: Subscription;
|
||||
id: number | -1;
|
||||
status: number | -1;
|
||||
start_date: string | '';
|
||||
end_date: string | '';
|
||||
parent_member_id: number | -1;
|
||||
subscription: Subscription;
|
||||
}
|
||||
|
||||
interface BankAccount {
|
||||
id: number | -1;
|
||||
mandate_date_signed: string | "";
|
||||
bank: string | "";
|
||||
account_holder_name: string | "";
|
||||
iban: string | "";
|
||||
bic: string | "";
|
||||
mandate_reference: string | "";
|
||||
id: number | -1;
|
||||
mandate_date_signed: string | '';
|
||||
bank: string | '';
|
||||
account_holder_name: string | '';
|
||||
iban: string | '';
|
||||
bic: string | '';
|
||||
mandate_reference: string | '';
|
||||
}
|
||||
|
||||
interface Licence {
|
||||
id: number | -1;
|
||||
status: number | -1;
|
||||
licence_number: string | "";
|
||||
issued_date: string | "";
|
||||
expiration_date: string | "";
|
||||
country: string | "";
|
||||
licence_categories: LicenceCategory[];
|
||||
id: number | -1;
|
||||
status: number | -1;
|
||||
number: string | '';
|
||||
issued_date: string | '';
|
||||
expiration_date: string | '';
|
||||
country: string | '';
|
||||
categories: LicenceCategory[];
|
||||
}
|
||||
|
||||
interface LicenceCategory {
|
||||
id: number | -1;
|
||||
category: string | "";
|
||||
id: number | -1;
|
||||
category: string | '';
|
||||
}
|
||||
|
||||
interface User {
|
||||
email: string | "";
|
||||
first_name: string | "";
|
||||
last_name: string | "";
|
||||
phone: string | "";
|
||||
notes: string | "";
|
||||
address: string | "";
|
||||
zip_code: string | "";
|
||||
city: string | "";
|
||||
status: number | -1;
|
||||
id: number | -1;
|
||||
role_id: number | -1;
|
||||
date_of_birth: string | "";
|
||||
company: string | "";
|
||||
profile_picture: string | "";
|
||||
payment_status: number | -1;
|
||||
membership: Membership;
|
||||
bank_account: BankAccount;
|
||||
licence: Licence;
|
||||
notes: string | "";
|
||||
email: string | '';
|
||||
first_name: string | '';
|
||||
last_name: string | '';
|
||||
password: string | '';
|
||||
phone: string | '';
|
||||
address: string | '';
|
||||
zip_code: string | '';
|
||||
city: string | '';
|
||||
status: number | -1;
|
||||
id: number | -1;
|
||||
role_id: number | -1;
|
||||
dateofbirth: string | '';
|
||||
company: string | '';
|
||||
membership: Membership | null;
|
||||
bank_account: BankAccount | null;
|
||||
licence: Licence | null;
|
||||
notes: string | '';
|
||||
}
|
||||
|
||||
interface Car {
|
||||
id: number | -1;
|
||||
name: string | '';
|
||||
status: number | 0;
|
||||
brand: string | '';
|
||||
model: string | '';
|
||||
price: number | 0;
|
||||
rate: number | 0;
|
||||
start_date: string | '';
|
||||
end_date: string | '';
|
||||
color: string | '';
|
||||
licence_plate: string | '';
|
||||
location: Location | null;
|
||||
damages: Damage[] | null;
|
||||
insurances: Insurance[] | null;
|
||||
notes: string | '';
|
||||
}
|
||||
|
||||
interface Location {
|
||||
latitude: number | 0;
|
||||
longitude: number | 0;
|
||||
}
|
||||
|
||||
interface Damage {
|
||||
id: number | -1;
|
||||
name: string | '';
|
||||
opponent: User | null;
|
||||
driver_id: number | -1;
|
||||
insurance: Insurance | null;
|
||||
date: string | '';
|
||||
notes: string | '';
|
||||
}
|
||||
|
||||
interface Insurance {
|
||||
id: number | -1;
|
||||
company: string | '';
|
||||
reference: string | '';
|
||||
start_date: string | '';
|
||||
end_date: string | '';
|
||||
notes: string | '';
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
user: User;
|
||||
users: User[];
|
||||
subscriptions: Subscription[];
|
||||
licence_categories: LicenceCategory[];
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
user: User;
|
||||
users: User[];
|
||||
cars: Cars[];
|
||||
subscriptions: Subscription[];
|
||||
licence_categories: LicenceCategory[];
|
||||
}
|
||||
interface Types {
|
||||
licenceCategory: LicenceCategory;
|
||||
subscription: Subscription;
|
||||
membership: Membership;
|
||||
licence: Licence;
|
||||
licenceCategory: LicenceCategory;
|
||||
bankAccount: BankAccount;
|
||||
car: Car;
|
||||
insurance: Insurance;
|
||||
location: Location;
|
||||
damage: Damage;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poiret+One&family=Quicksand:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<!-- <link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/> -->
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
@@ -1,60 +1,59 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants.js";
|
||||
import { refreshCookie, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI } from '$lib/utils/constants.js';
|
||||
import { refreshCookie, userDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
if (event.locals.user) {
|
||||
// if there is already a user in session load page as normal
|
||||
return await resolve(event);
|
||||
}
|
||||
if (event.locals.user) {
|
||||
// if there is already a user in session load page as normal
|
||||
console.log('user is logged in');
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
// get cookies from browser
|
||||
const jwt = event.cookies.get("jwt");
|
||||
// get cookies from browser
|
||||
const jwt = event.cookies.get('jwt');
|
||||
|
||||
if (!jwt) {
|
||||
// if there is no jwt load page as normal
|
||||
return await resolve(event);
|
||||
}
|
||||
const response = await fetch(`${BASE_API_URI}/backend/users/current`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
event.cookies.delete("jwt", { path: "/" });
|
||||
return await resolve(event);
|
||||
}
|
||||
if (!jwt) {
|
||||
// if there is no jwt load page as normal
|
||||
return await resolve(event);
|
||||
}
|
||||
const response = await fetch(`${BASE_API_URI}/auth/users/current`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
event.cookies.delete('jwt', { path: '/' });
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
refreshCookie(newToken, event);
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get('Set-Cookie');
|
||||
refreshCookie(newToken, event.cookies);
|
||||
|
||||
userDatesFromRFC3339(data.user);
|
||||
userDatesFromRFC3339(data.user);
|
||||
|
||||
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
]);
|
||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||
subscriptionsResponse.json(),
|
||||
licenceCategoriesResponse.json(),
|
||||
]);
|
||||
event.locals.user = data.user;
|
||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
||||
// console.log("hooks.server: Printing locals:");
|
||||
// console.dir(event.locals);
|
||||
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/auth/subscriptions`, {
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/auth/licence/categories`, {
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
})
|
||||
]);
|
||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||
subscriptionsResponse.json(),
|
||||
licenceCategoriesResponse.json()
|
||||
]);
|
||||
event.locals.user = data.user;
|
||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
||||
|
||||
// load page as normal
|
||||
return await resolve(event);
|
||||
// load page as normal
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
682
frontend/src/lib/components/CarEditForm.svelte
Normal file
682
frontend/src/lib/components/CarEditForm.svelte
Normal file
@@ -0,0 +1,682 @@
|
||||
<script>
|
||||
import InputField from '$lib/components/InputField.svelte';
|
||||
import SmallLoader from '$lib/components/SmallLoader.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { defaultDamage, defaultInsurance, defaultOpponent } from '$lib/utils/defaults';
|
||||
import { PERMISSIONS } from '$lib/utils/constants';
|
||||
import Modal from './Modal.svelte';
|
||||
import UserEditForm from './UserEditForm.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/** @type {App.Locals['user'] } */
|
||||
export let editor;
|
||||
|
||||
/** @type {App.Locals['users'] } */
|
||||
export let users;
|
||||
|
||||
/** @type {App.Types['car']} */
|
||||
export let car;
|
||||
|
||||
$: console.log(
|
||||
'damage.opponent changed:',
|
||||
car?.damages.map((d) => d.opponent)
|
||||
);
|
||||
$: console.log(
|
||||
'damage.insurance changed:',
|
||||
car?.damages.map((d) => d.insurance)
|
||||
);
|
||||
// TODO: Remove when working
|
||||
// $: if (car.damages.length > 0 && !car.damages.every((d) => d.insurance && d.opponent)) {
|
||||
// car.damages = car.damages.map((damage) => ({
|
||||
// ...damage,
|
||||
// insurance: damage.insurance ?? defaultInsurance(),
|
||||
// opponent: damage.opponent ?? defaultOpponent()
|
||||
// }));
|
||||
// }
|
||||
let initialized = false; // Prevents infinite loops
|
||||
|
||||
// Ensure damages have default values once `car` is loaded
|
||||
$: if (car && !initialized) {
|
||||
car = {
|
||||
...car,
|
||||
damages:
|
||||
car.damages?.map((damage) => ({
|
||||
...damage,
|
||||
insurance: damage.insurance ?? defaultInsurance(),
|
||||
opponent: damage.opponent ?? defaultOpponent()
|
||||
})) || []
|
||||
};
|
||||
initialized = true; // Prevents re-running
|
||||
}
|
||||
$: isLoading = car === undefined || editor === undefined;
|
||||
let isUpdating = false;
|
||||
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
|
||||
|
||||
/** @type {number | null} */
|
||||
let editingUserIndex = null;
|
||||
|
||||
const TABS = ['car.car', 'insurance', 'car.damages'];
|
||||
let activeTab = TABS[0];
|
||||
|
||||
/** @type {import('@sveltejs/kit').SubmitFunction} */
|
||||
const handleUpdate = async () => {
|
||||
isUpdating = true;
|
||||
return async ({ result }) => {
|
||||
isUpdating = false;
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
dispatch('close');
|
||||
} else {
|
||||
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<SmallLoader width={30} message={$t('loading.car_data')} />
|
||||
{:else if editor && car}
|
||||
<form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}>
|
||||
<input name="car[id]" type="hidden" bind:value={car.id} />
|
||||
<h1 class="step-title" style="text-align: center;">
|
||||
{car.id ? $t('car.edit') : $t('car.create')}
|
||||
</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive|global={{ key: error.id }}
|
||||
out:send|global={{ key: error.id }}
|
||||
>
|
||||
{$t(error.field) + ': ' + $t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="button-container">
|
||||
{#each TABS as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="button-dark"
|
||||
class:active={activeTab === tab}
|
||||
on:click={() => (activeTab = tab)}
|
||||
>
|
||||
{$t(tab)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tab-content" style="display: {activeTab === 'car.car' ? 'block' : 'none'}">
|
||||
<InputField
|
||||
name="car[name]"
|
||||
label={$t('name')}
|
||||
bind:value={car.name}
|
||||
placeholder={$t('placeholder.car_name')}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[brand]"
|
||||
label={$t('car.brand')}
|
||||
bind:value={car.brand}
|
||||
placeholder={$t('placeholder.car_brand')}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[model]"
|
||||
label={$t('car.model')}
|
||||
bind:value={car.model}
|
||||
placeholder={$t('placeholder.car_model')}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[color]"
|
||||
label={$t('color')}
|
||||
bind:value={car.color}
|
||||
placeholder={$t('placeholder.car_color')}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[licence_plate]"
|
||||
label={$t('car.licence_plate')}
|
||||
bind:value={car.licence_plate}
|
||||
placeholder={$t('placeholder.car_licence_plate')}
|
||||
required={true}
|
||||
toUpperCase={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[price]"
|
||||
type="number"
|
||||
label={$t('price')}
|
||||
bind:value={car.price}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[rate]"
|
||||
type="number"
|
||||
label={$t('car.leasing_rate')}
|
||||
bind:value={car.rate}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[start_date]"
|
||||
type="date"
|
||||
label={$t('car.start_date')}
|
||||
bind:value={car.start_date}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[end_date]"
|
||||
type="date"
|
||||
label={$t('car.end_date')}
|
||||
bind:value={car.end_date}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[notes]"
|
||||
type="textarea"
|
||||
label={$t('notes')}
|
||||
bind:value={car.notes}
|
||||
placeholder={$t('placeholder.notes', {
|
||||
values: { name: car.name || car.brand + ' ' + car.model }
|
||||
})}
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
<div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}">
|
||||
<div class="accordion">
|
||||
{#each car.insurances as insurance, index}
|
||||
<input hidden value={insurance?.id} name="car[insurances][{index}][id]" />
|
||||
<details class="accordion-item" open={index === car.insurances.length - 1}>
|
||||
<summary class="accordion-header">
|
||||
{insurance.company ? insurance.company : ''}
|
||||
{insurance.reference ? ' (' + insurance.reference + ')' : ''}
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<InputField
|
||||
name="car[insurances][{index}][company]"
|
||||
label={$t('company')}
|
||||
bind:value={insurance.company}
|
||||
placeholder={$t('placeholder.company')}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[insurances][{index}][reference]"
|
||||
label={$t('insurance_reference')}
|
||||
bind:value={insurance.reference}
|
||||
placeholder={$t('placeholder.insurance_reference')}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[insurances][{index}][start_date]"
|
||||
type="date"
|
||||
label={$t('start')}
|
||||
bind:value={insurance.start_date}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[insurances][{index}][end_date]"
|
||||
type="date"
|
||||
label={$t('end')}
|
||||
bind:value={insurance.end_date}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[insurances][{index}][notes]"
|
||||
type="textarea"
|
||||
label={$t('notes')}
|
||||
bind:value={insurance.notes}
|
||||
placeholder={$t('placeholder.notes', {
|
||||
values: { name: insurance.company || '' }
|
||||
})}
|
||||
rows={10}
|
||||
/>
|
||||
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-delete danger"
|
||||
on:click={() => {
|
||||
if (
|
||||
confirm(
|
||||
$t('dialog.insurance_deletion', {
|
||||
values: {
|
||||
name: insurance.company + ' (' + insurance.reference + ')'
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
car.insurances = car.insurances.filter((_, i) => i !== index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="button-group">
|
||||
{#if hasPrivilige(editor, PERMISSIONS.Create)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
car.insurances = [...car.insurances, defaultInsurance()];
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" style="display: {activeTab === 'car.damages' ? 'block' : 'none'}">
|
||||
<div class="accordion">
|
||||
{#each car.damages as damage, index (damage.id)}
|
||||
<input type="hidden" name="car[damages][{index}][id]" value={damage.id} />
|
||||
<details class="accordion-item" open={index === car.damages.length - 1}>
|
||||
<summary class="accordion-header">
|
||||
<span class="nav-badge">
|
||||
{damage.name} -
|
||||
{damage.opponent.first_name}
|
||||
{damage.opponent.last_name}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<InputField
|
||||
name="car[damages][{index}][date]"
|
||||
type="date"
|
||||
label={$t('date')}
|
||||
bind:value={damage.date}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[damages][{index}][name]"
|
||||
label={$t('car.damages')}
|
||||
bind:value={damage.name}
|
||||
required={true}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[damages][{index}][driver_id]"
|
||||
type="select"
|
||||
label={$t('user.member')}
|
||||
options={users
|
||||
?.filter((u) => u.role_id > 0)
|
||||
.map((u) => ({
|
||||
value: u.id,
|
||||
label: `${u.first_name} ${u.last_name}`,
|
||||
color: '--subtext1'
|
||||
})) || []}
|
||||
bind:value={damage.driver_id}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<h4>{$t('user.opponent')}</h4>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][id]`}
|
||||
value={car.damages[index].opponent.id}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][email]`}
|
||||
value={car.damages[index].opponent.email}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][first_name]`}
|
||||
value={car.damages[index].opponent.first_name}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][last_name]`}
|
||||
value={damage.opponent.last_name}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][phone]`}
|
||||
value={damage.opponent.phone}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][address]`}
|
||||
value={damage.opponent.address}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][city]`}
|
||||
value={damage.opponent.city}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][zip_code]`}
|
||||
value={damage.opponent.zip_code}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][notes]`}
|
||||
value={damage.opponent.notes}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][role_id]`}
|
||||
value={damage.opponent.role_id}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][status]`}
|
||||
value={damage.opponent.status}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][dateofbirth]`}
|
||||
value={damage.opponent.dateofbirth}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][company]`}
|
||||
value={damage.opponent.company}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][id]`}
|
||||
value={damage.opponent.bank_account.id}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][mandate_date_signed]`}
|
||||
value={damage.opponent.bank_account.mandate_date_signed}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][bank]`}
|
||||
value={damage.opponent.bank_account.bank}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][account_holder_name]`}
|
||||
value={damage.opponent.bank_account.account_holder_name}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][iban]`}
|
||||
value={damage.opponent.bank_account.iban}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][bic]`}
|
||||
value={damage.opponent.bank_account.bic}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][opponent][bank_account][mandate_reference]`}
|
||||
value={damage.opponent.bank_account.mandate_reference}
|
||||
/>
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
<span class="nav-badge">
|
||||
{#if damage.opponent?.first_name}
|
||||
{damage.opponent.first_name} {damage.opponent.last_name}
|
||||
{:else}
|
||||
{$t('not_set')}
|
||||
{/if}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{$t('email')}</th>
|
||||
<td>{damage.opponent?.email || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('phone')}</th>
|
||||
<td>{damage.opponent?.phone || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('address')}</th>
|
||||
<td>{damage.opponent?.address || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('city')}</th>
|
||||
<td>{damage.opponent?.city || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('zip_code')}</th>
|
||||
<td>{damage.opponent?.zip_code || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
if (!damage.opponent) {
|
||||
damage.opponent = defaultOpponent();
|
||||
}
|
||||
editingUserIndex = index;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{damage.opponent?.id ? $t('edit') : $t('edit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<input
|
||||
hidden
|
||||
name={`car[damages][${index}][insurance][id]`}
|
||||
value={damage.insurance.id}
|
||||
/>
|
||||
<input hidden name={`car[damages][${index}][insurance][start_date]`} value="" />
|
||||
<input hidden name={`car[damages][${index}][insurance][end_date]`} value="" />
|
||||
<InputField
|
||||
name="car[damages][{index}][insurance][company]"
|
||||
label={$t('insurance')}
|
||||
bind:value={damage.insurance.company}
|
||||
placeholder={$t('placeholder.company')}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[damages][{index}][insurance][reference]"
|
||||
label={$t('insurance_reference')}
|
||||
bind:value={damage.insurance.reference}
|
||||
placeholder={$t('placeholder.insurance_reference')}
|
||||
readonly={readonlyUser}
|
||||
/>
|
||||
<InputField
|
||||
name="car[damages][{index}][notes]"
|
||||
type="textarea"
|
||||
label={$t('notes')}
|
||||
bind:value={damage.notes}
|
||||
placeholder={$t('placeholder.notes')}
|
||||
rows={10}
|
||||
/>
|
||||
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-delete danger"
|
||||
on:click={() => {
|
||||
if (
|
||||
confirm(
|
||||
$t('dialog.damage_deletion', {
|
||||
values: {
|
||||
name: damage.name
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
car.damages = car.damages.filter((_, i) => i !== index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{#if hasPrivilige(editor, PERMISSIONS.Create)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
car.damages = [...car.damages, defaultDamage()];
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="button-container">
|
||||
{#if isUpdating}
|
||||
<SmallLoader width={30} message={$t('loading.updating')} />
|
||||
{:else}
|
||||
<button type="button" class="button-dark" on:click={() => dispatch('cancel')}>
|
||||
{$t('cancel')}</button
|
||||
>
|
||||
<button type="submit" class="button-dark">{$t('confirm')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if editingUserIndex !== null}
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
submit_form={false}
|
||||
subscriptions={null}
|
||||
licence_categories={null}
|
||||
{editor}
|
||||
bind:user={car.damages[editingUserIndex].opponent}
|
||||
on:cancel={() => (editingUserIndex = null)}
|
||||
on:close={() => {
|
||||
car.damages = car.damages;
|
||||
editingUserIndex = null;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.accordion-item {
|
||||
border: none;
|
||||
background: var(--surface0);
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
color: var(--text);
|
||||
background: var(--surface1);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
padding: 1rem;
|
||||
background: var(--surface0);
|
||||
border-top: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.accordion-content .table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.accordion-content .table th,
|
||||
.accordion-content .table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #2f2f2f;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.accordion-content .table th {
|
||||
color: var(--subtext1);
|
||||
}
|
||||
|
||||
.accordion-content .table td {
|
||||
color: var(--text);
|
||||
}
|
||||
.button-container button.active {
|
||||
background-color: var(--mauve);
|
||||
border-color: var(--mauve);
|
||||
color: var(--base);
|
||||
}
|
||||
.btn-delete {
|
||||
margin-left: auto;
|
||||
}
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background-color: var(--surface0);
|
||||
border: 1px solid var(--surface1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.tab-content h4 {
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
color: var(--lavender);
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container button {
|
||||
flex: 1 1 0;
|
||||
min-width: 120px;
|
||||
max-width: calc(50% - 5px);
|
||||
background-color: var(--surface1);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--overlay0);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.button-container button:hover {
|
||||
background-color: var(--surface2);
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.button-container button {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +1,22 @@
|
||||
<script>
|
||||
// import Developer from "$lib/img/hero-image.png";
|
||||
// import Developer from "$lib/img/hero-image.png";
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
const year = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<footer class="footer-container">
|
||||
<div class="footer-branding-container">
|
||||
<div class="footer-branding">
|
||||
<a class="footer-crafted-by-container" href="https://github.com/Sirneij">
|
||||
<span>Developed by</span>
|
||||
<!-- <img
|
||||
<div class="footer-branding-container">
|
||||
<div class="footer-branding">
|
||||
<a class="footer-crafted-by-container" href="https://github.com/17Halbe">
|
||||
<span>Developed by</span>
|
||||
<!-- <img
|
||||
class="footer-branded-crafted-img"
|
||||
src={Developer}
|
||||
alt="Alexander Stölting"
|
||||
/> -->
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<span class="footer-copyright"
|
||||
>© {year} Alexander Stölting. All Rights Reserved.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<span class="footer-copyright">© {year} Alexander Stölting. All Rights Reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,68 +1,120 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { applyAction, enhance } from "$app/forms";
|
||||
import { page } from "$app/stores";
|
||||
// import Developer from "$lib/img/hero-image.png";
|
||||
// import Avatar from "$lib/img/TeamAvatar.jpeg";
|
||||
import { t } from "svelte-i18n";
|
||||
onMount(() => {
|
||||
console.log("Page data in Header:", $page);
|
||||
});
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { writable } from 'svelte/store';
|
||||
import { PERMISSIONS } from '$lib/utils/constants';
|
||||
import { hasPrivilige } from '$lib/utils/helpers';
|
||||
|
||||
$: {
|
||||
console.log("Page data updated:", $page);
|
||||
}
|
||||
let isMobileMenuOpen = false;
|
||||
|
||||
/** @type{HTMLDivElement} */
|
||||
let headerContainer;
|
||||
|
||||
onMount(() => {
|
||||
console.log('Page data in Header:', $page);
|
||||
document.documentElement.setAttribute('data-theme', $theme);
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
$: {
|
||||
console.log('Page data updated:', $page);
|
||||
}
|
||||
|
||||
const theme = writable(
|
||||
typeof window !== 'undefined' ? localStorage.getItem('theme') || 'dark' : 'dark'
|
||||
);
|
||||
|
||||
/**
|
||||
* handle a click outside the menu to close it.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function handleClickOutside(event) {
|
||||
if (
|
||||
isMobileMenuOpen &&
|
||||
event.target instanceof Node &&
|
||||
!!headerContainer.contains(event.target)
|
||||
) {
|
||||
isMobileMenuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen = !isMobileMenuOpen;
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.update((current) => {
|
||||
const newTheme = current === 'dark' ? 'bright' : 'dark';
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
return newTheme;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-container">
|
||||
<div class="header-left">
|
||||
<div class="header-crafted-by-container">
|
||||
<!-- <a href="https://tiny-bits.net/">
|
||||
<div class="header-container" bind:this={headerContainer}>
|
||||
<div class="header-left">
|
||||
<div class="header-crafted-by-container">
|
||||
<!-- <a href="https://tiny-bits.net/">
|
||||
<span>Developed by</span><img
|
||||
src={Developer}
|
||||
alt="Alexander Stölting"
|
||||
/> -->
|
||||
<!-- </a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-nav-item" class:active={$page.url.pathname === "/"}>
|
||||
<a href="/">home</a>
|
||||
</div>
|
||||
{#if !$page.data.user}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname === "/auth/login"}
|
||||
>
|
||||
<a href="/auth/login">login</a>
|
||||
</div>
|
||||
<!-- <div
|
||||
<!-- </a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-menu-container">
|
||||
<button
|
||||
class="mobile-menu-toggle"
|
||||
on:click={toggleMobileMenu}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
>
|
||||
<i class="fas {isMobileMenuOpen ? 'fa-times' : 'fa-bars'}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="header-right" class:mobile-menu-open={isMobileMenuOpen}>
|
||||
<div class="header-nav-item" class:active={$page.url.pathname === '/'}>
|
||||
<a href={`${base}/`}>home</a>
|
||||
</div>
|
||||
{#if !$page.data.user}
|
||||
<div class="header-nav-item" class:active={$page.url.pathname === `${base}/auth/login`}>
|
||||
<a href={`${base}/auth/login`}>login</a>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname === "/auth/register"}
|
||||
>
|
||||
<a href="/auth/register">register</a>
|
||||
</div> -->
|
||||
{:else}
|
||||
<div class="header-nav-item">
|
||||
<a href="/auth/about/{$page.data.user.id}">
|
||||
<!-- <img
|
||||
{:else}
|
||||
<div class="header-nav-item">
|
||||
<a href={`${base}/auth/about/${$page.data.user.id}`}>
|
||||
<!-- <img
|
||||
src={$page.data.user.profile_picture ? $page.data.user.profile_picture : Avatar}
|
||||
alt={`${$page.data.user.first_name} ${$page.data.user.last_name}`}
|
||||
/> -->
|
||||
{$page.data.user.first_name}
|
||||
{$page.data.user.last_name}
|
||||
</a>
|
||||
</div>
|
||||
{#if $page.data.user.role_id > 0}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname.startsWith("/auth/admin/users")}
|
||||
>
|
||||
<a href="/auth/admin/users">{$t("user.management")}</a>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- {#if $page.data.user.is_superuser}
|
||||
{$page.data.user.first_name}
|
||||
{$page.data.user.last_name}
|
||||
</a>
|
||||
</div>
|
||||
{#if hasPrivilige($page.data.user, PERMISSIONS.View)}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname.startsWith(`${base}/auth/admin/users`)}
|
||||
>
|
||||
<a href={`${base}/auth/admin/users`}>{$t('user.management')}</a>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- {#if $page.data.user.is_superuser}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname.startsWith("/auth/admin")}
|
||||
@@ -70,19 +122,277 @@
|
||||
<a href="/auth/admin">admin</a>
|
||||
</div>
|
||||
{/if} -->
|
||||
<form
|
||||
class="header-nav-item"
|
||||
action="/auth/logout"
|
||||
method="POST"
|
||||
use:enhance={async () => {
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
};
|
||||
}}
|
||||
>
|
||||
<button type="submit">logout</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
class="header-nav-item"
|
||||
action={`${base}/auth/logout`}
|
||||
method="POST"
|
||||
use:enhance={async () => {
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
};
|
||||
}}
|
||||
>
|
||||
<button type="submit">logout</button>
|
||||
</form>
|
||||
{/if}
|
||||
<div class="theme-toggle">
|
||||
<label class="switch">
|
||||
<input type="checkbox" checked={$theme === 'bright'} on:change={toggleTheme} />
|
||||
<span class="slider">
|
||||
<i class="fas fa-sun"></i>
|
||||
<i class="fas fa-moon"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--surface0);
|
||||
transition: 0.4s;
|
||||
border-radius: 30px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--text);
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--surface0);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.fa-sun,
|
||||
.fa-moon {
|
||||
position: absolute;
|
||||
font-size: 16px;
|
||||
top: 7px;
|
||||
color: var(--text);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.fa-sun {
|
||||
left: 7px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fa-moon {
|
||||
right: 7px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:checked + .slider .fa-sun {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:checked + .slider .fa-moon {
|
||||
opacity: 0;
|
||||
}
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.mobile-menu-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 3em 0 0;
|
||||
background: var(--base);
|
||||
}
|
||||
.header .header-container {
|
||||
width: 100%;
|
||||
max-width: calc(1200px + 10em);
|
||||
height: 5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
background-color: var(--base);
|
||||
}
|
||||
.header .header-container .header-left {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.header .header-container .header-right {
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item {
|
||||
text-transform: uppercase;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header .header-container .header-right .header-nav-item a,
|
||||
.header .header-container .header-right .header-nav-item button {
|
||||
transition: color 0.3s ease-in-out;
|
||||
display: block;
|
||||
padding: 20px 0;
|
||||
border: none;
|
||||
color: var(--subtext0);
|
||||
}
|
||||
|
||||
.header .header-container .header-right .header-nav-item:hover a,
|
||||
.header .header-container .header-right .header-nav-item:hover button {
|
||||
color: var(--lavender);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.header {
|
||||
padding: 3em 5rem 0;
|
||||
}
|
||||
.header .header-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
.header .header-container .header-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item {
|
||||
margin-left: 26px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.mobile-menu-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header .header-container {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
padding: 0 1rem;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: none;
|
||||
top: 4rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--base);
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.header-right.mobile-menu-open {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-nav-item {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.header-nav-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.header .header-container .header-right {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header .header-container .header-right .header-nav-item {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.header .header-container .header-right .header-nav-item a,
|
||||
.header .header-container .header-right .header-nav-item button {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,304 +1,343 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { t } from "svelte-i18n";
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
/** @type {string} */
|
||||
export let name;
|
||||
/** @type {string} */
|
||||
export let name;
|
||||
|
||||
/** @type {string} */
|
||||
export let type = "text";
|
||||
/** @type {string} */
|
||||
export let type = 'text';
|
||||
|
||||
/** @type {string|Number|null} */
|
||||
export let value;
|
||||
/** @type {string|Number|null} */
|
||||
export let value;
|
||||
|
||||
/** @type {string} */
|
||||
export let placeholder = "";
|
||||
/** @type {string} */
|
||||
export let placeholder = '';
|
||||
|
||||
/** @type {Number} */
|
||||
export let rows = 4;
|
||||
/** @type {Number} */
|
||||
export let rows = 4;
|
||||
|
||||
/** @type {Array<{value: string | number, label: string, color?:string}>} */
|
||||
export let options = [];
|
||||
/** @type {Array<{value: string | number, label: string, color?:string}>} */
|
||||
export let options = [];
|
||||
|
||||
/** @type {Boolean} */
|
||||
export let required = false;
|
||||
/** @type {Boolean} */
|
||||
export let required = false;
|
||||
|
||||
/** @type {string} */
|
||||
export let label = "";
|
||||
/** @type {string} */
|
||||
export let label = '';
|
||||
|
||||
/** @type {string} */
|
||||
export let otherPasswordValue = "";
|
||||
/** @type {string} */
|
||||
export let otherPasswordValue = '';
|
||||
|
||||
/** @type {boolean} */
|
||||
export let toUpperCase = false;
|
||||
/** @type {boolean} */
|
||||
export let toUpperCase = false;
|
||||
|
||||
/** @type {boolean} */
|
||||
export let checked = false;
|
||||
/** @type {boolean} */
|
||||
export let checked = false;
|
||||
|
||||
/** @type {boolean} */
|
||||
export let readonly = false;
|
||||
/** @type {boolean} */
|
||||
export let readonly = false;
|
||||
|
||||
/**
|
||||
* @param {Event} event - The input event
|
||||
*/
|
||||
function handleInput(event) {
|
||||
const target = event.target;
|
||||
/** @type {string} */
|
||||
export let backgroundColor = '--surface0';
|
||||
|
||||
if (target instanceof HTMLInputElement) {
|
||||
let inputValue = target.value;
|
||||
if (toUpperCase) {
|
||||
inputValue = inputValue.toUpperCase();
|
||||
target.value = inputValue; // Update the input field value
|
||||
}
|
||||
value = inputValue;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Event} event - The input event
|
||||
*/
|
||||
function handleInput(event) {
|
||||
const target = event.target;
|
||||
|
||||
/**
|
||||
* Validates the field
|
||||
* @param {string} name - The name of the field
|
||||
* @param {string|Number|null} value - The value of the field
|
||||
* @param {Boolean} required - The requirements of the field
|
||||
* @returns {string|null} The error message or null if valid
|
||||
*/
|
||||
function validateField(name, value, required) {
|
||||
if (
|
||||
value === null ||
|
||||
(typeof value === "string" && !value.trim() && !required)
|
||||
)
|
||||
return null;
|
||||
switch (name) {
|
||||
case "membership_start_date":
|
||||
return typeof value === "string" && value.trim()
|
||||
? null
|
||||
: $t("validation.date");
|
||||
case "email":
|
||||
return typeof value === "string" && /^\S+@\S+\.\S+$/.test(value)
|
||||
? null
|
||||
: $t("validation.email");
|
||||
case "password":
|
||||
case "password2":
|
||||
if (typeof value === "string" && value.length < 8) {
|
||||
return $t("validation.password");
|
||||
}
|
||||
if (otherPasswordValue && value !== otherPasswordValue) {
|
||||
return $t("validation.password_match");
|
||||
}
|
||||
return null;
|
||||
case "phone":
|
||||
return typeof value === "string" && /^\+?[0-9\s()-]{7,}$/.test(value)
|
||||
? null
|
||||
: $t("validation.phone");
|
||||
case "zip_code":
|
||||
return typeof value === "string" && /^\d{5}$/.test(value)
|
||||
? null
|
||||
: $t("validation.zip_code");
|
||||
case "iban":
|
||||
return typeof value === "string" &&
|
||||
/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
|
||||
? null
|
||||
: $t("validation.iban");
|
||||
case "bic":
|
||||
return typeof value === "string" &&
|
||||
/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
||||
? null
|
||||
: $t("validation.bic");
|
||||
case "licence_number":
|
||||
return typeof value === "string" && value.length == 11
|
||||
? null
|
||||
: $t("validation.licence");
|
||||
if (target instanceof HTMLInputElement) {
|
||||
let inputValue = target.value;
|
||||
if (toUpperCase) {
|
||||
inputValue = inputValue.toUpperCase();
|
||||
}
|
||||
target.value = inputValue; // Update the input field value
|
||||
value = inputValue.trim();
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return typeof value === "string" && !value.trim() && required
|
||||
? $t("validation.required")
|
||||
: null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validates the field
|
||||
* @param {string} name - The name of the field
|
||||
* @param {string|Number|null} value - The value of the field
|
||||
* @param {Boolean} required - The requirements of the field
|
||||
* @returns {string|null} The error message or null if valid
|
||||
*/
|
||||
function validateField(name, value, required) {
|
||||
if (value === null || (typeof value === 'string' && !value.trim() && !required)) return null;
|
||||
if (name.includes('membership_start_date')) {
|
||||
return typeof value === 'string' && value.trim() ? null : $t('validation.date');
|
||||
} else if (name.includes('email')) {
|
||||
return typeof value === 'string' && /^\S+@\S+\.\S+$/.test(value)
|
||||
? null
|
||||
: $t('validation.email');
|
||||
} else if (name.includes('password')) {
|
||||
if (typeof value === 'string' && value.length < 8) {
|
||||
return $t('validation.password');
|
||||
}
|
||||
if (otherPasswordValue && value !== otherPasswordValue) {
|
||||
return $t('validation.password_match');
|
||||
}
|
||||
return null;
|
||||
} else if (name.includes('phone')) {
|
||||
return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value)
|
||||
? null
|
||||
: $t('validation.phone');
|
||||
} else if (name.includes('zip_code')) {
|
||||
return typeof value === 'string' && /^\d{5}$/.test(value) ? null : $t('validation.zip_code');
|
||||
} else if (name.includes('iban')) {
|
||||
return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
|
||||
? null
|
||||
: $t('validation.iban');
|
||||
} else if (name.includes('bic')) {
|
||||
return typeof value === 'string' && /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
||||
? null
|
||||
: $t('validation.bic');
|
||||
} else if (name.includes('licence_number')) {
|
||||
return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
|
||||
} else {
|
||||
return typeof value === 'string' && !value.trim() && required
|
||||
? $t('validation.required')
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
$: error = validateField(name, value, required);
|
||||
$: selectedOption = options.find((option) => option.value == value);
|
||||
$: selectedColor = selectedOption ? selectedOption.color : "";
|
||||
$: error = validateField(name, value, required);
|
||||
$: selectedOption = options.find((option) => option.value == value);
|
||||
$: selectedColor = selectedOption ? `var(${selectedOption.color})` : '';
|
||||
</script>
|
||||
|
||||
<div class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}">
|
||||
{#if type === "checkbox"}
|
||||
<label class="form-control {readonly ? 'form-control--disabled' : ''}">
|
||||
<input
|
||||
type="checkbox"
|
||||
{name}
|
||||
{value}
|
||||
{checked}
|
||||
{readonly}
|
||||
on:change={() => (checked = !checked)}
|
||||
/>
|
||||
<span class="checkbox-text"> {label} </span>
|
||||
</label>
|
||||
{:else}
|
||||
<span class="label">{label}</span>
|
||||
{/if}
|
||||
<div class="input-error-container">
|
||||
{#if error}
|
||||
<span class="error-message">{error}</span>
|
||||
{/if}
|
||||
{#if type === "select"}
|
||||
<select
|
||||
{name}
|
||||
bind:value
|
||||
{required}
|
||||
class="input select"
|
||||
style={selectedColor ? `color: ${selectedColor};` : ""}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if type === "textarea"}
|
||||
<textarea
|
||||
{name}
|
||||
{placeholder}
|
||||
{required}
|
||||
{value}
|
||||
{readonly}
|
||||
{rows}
|
||||
class="input textarea {readonly ? 'readonly' : ''}"
|
||||
style="height:{rows * 1.5}em;"
|
||||
/>
|
||||
{:else if type != "checkbox"}
|
||||
<input
|
||||
{name}
|
||||
{type}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
{value}
|
||||
{required}
|
||||
on:input={handleInput}
|
||||
on:blur={handleInput}
|
||||
class="input {readonly ? 'readonly' : ''}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}"
|
||||
style="background-color: var({backgroundColor});"
|
||||
>
|
||||
{#if type === 'checkbox'}
|
||||
<label class="form-control {readonly ? 'form-control--disabled' : ''}">
|
||||
<input
|
||||
type="checkbox"
|
||||
{name}
|
||||
{value}
|
||||
{checked}
|
||||
{readonly}
|
||||
on:change={() => (checked = !checked)}
|
||||
/>
|
||||
<span class="checkbox-text"> {label} </span>
|
||||
</label>
|
||||
{:else}
|
||||
<span class="label">{label}</span>
|
||||
{/if}
|
||||
<div class="input-error-container">
|
||||
{#if error}
|
||||
<span class="error-message">{error}</span>
|
||||
{/if}
|
||||
{#if readonly}
|
||||
<input {name} type="hidden" bind:value />
|
||||
|
||||
<span class="label"
|
||||
>{type == 'select' && typeof value === 'number' ? options[value].label : value}</span
|
||||
>
|
||||
{:else if type === 'select'}
|
||||
<select
|
||||
{name}
|
||||
bind:value
|
||||
{required}
|
||||
class="input select"
|
||||
style={selectedColor ? `color: ${selectedColor};` : ''}
|
||||
disabled={readonly}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if type === 'textarea'}
|
||||
<textarea
|
||||
{name}
|
||||
{placeholder}
|
||||
{required}
|
||||
{value}
|
||||
{readonly}
|
||||
{rows}
|
||||
class="input textarea {readonly ? 'readonly' : ''}"
|
||||
style="height:{rows * 1.5}em;"
|
||||
></textarea>
|
||||
{:else if type != 'checkbox'}
|
||||
<input
|
||||
{name}
|
||||
{type}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
{value}
|
||||
{required}
|
||||
on:input={handleInput}
|
||||
on:blur={handleInput}
|
||||
class="input {readonly ? 'readonly' : ''}"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--form-control-color: #6bff55;
|
||||
--form-control-disabled: #959495;
|
||||
}
|
||||
:root {
|
||||
--form-control-color: var(--green); /* Changed from #6bff55 */
|
||||
--form-control-disabled: var(--subtext1); /* Changed from #959495 */
|
||||
}
|
||||
|
||||
.form-control {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.1;
|
||||
display: grid;
|
||||
grid-template-columns: 1.5em auto;
|
||||
gap: 0.75em;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.form-control {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.1;
|
||||
display: grid;
|
||||
grid-template-columns: 1.5em auto;
|
||||
gap: 0.75em;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.form-control--disabled {
|
||||
color: var(--form-control-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.form-control--disabled {
|
||||
color: var(--form-control-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--form-background);
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: currentColor;
|
||||
width: 1.75em;
|
||||
height: 1.75em;
|
||||
border: 0.15em solid currentColor;
|
||||
border-radius: 0.5em;
|
||||
transform: translateY(-0.075em);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--surface0);
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
width: 1.75em;
|
||||
height: 1.75em;
|
||||
border: 0.15em solid var(--overlay0);
|
||||
border-radius: 0.5em;
|
||||
transform: translateY(-0.075em);
|
||||
display: grid;
|
||||
place-content: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
input[type="checkbox"]::before {
|
||||
content: "";
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
transform: scale(0);
|
||||
transform-origin: bottom left;
|
||||
transition: 120ms transform ease-in-out;
|
||||
box-shadow: inset 1em 1em var(--form-control-color);
|
||||
background-color: CanvasText;
|
||||
}
|
||||
input[type='checkbox']::before {
|
||||
content: '';
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
transform: scale(0);
|
||||
transform-origin: bottom left;
|
||||
transition: 120ms transform ease-in-out;
|
||||
box-shadow: inset 1em 1em var(--form-control-color);
|
||||
background-color: var(--crust);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
input[type='checkbox']:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:hover {
|
||||
outline: max(2px, 0.15em) solid currentColor;
|
||||
outline-offset: max(2px, 0.15em);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
input[type='checkbox']:hover {
|
||||
outline: max(2px, 0.15em) solid var(--lavender);
|
||||
outline-offset: max(2px, 0.15em);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled {
|
||||
--form-control-color: var(--form-control-disabled);
|
||||
color: var(--form-control-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.readonly {
|
||||
background-color: #ececec;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
color: #4f4f4f;
|
||||
}
|
||||
input[type='checkbox']:disabled {
|
||||
--form-control-color: var(--form-control-disabled);
|
||||
color: var(--form-control-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.readonly {
|
||||
background-color: var(--surface0);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
color: var(--overlay1);
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
.checkbox-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
.checkbox-text {
|
||||
font-size: 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.checkbox-text {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.select {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
.input-error-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.checkbox-text {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.select {
|
||||
padding-right: 1.5em;
|
||||
background-color: var(--surface0);
|
||||
font-weight: bold;
|
||||
}
|
||||
.input-error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #eb5424;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.error-message {
|
||||
color: var(--red); /* Changed from #eb5424 */
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem 0;
|
||||
background-color: var(--surface0);
|
||||
border: 1px solid var(--overlay0);
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
|
||||
input:hover:not(:disabled),
|
||||
textarea:hover:not(:disabled),
|
||||
select:hover:not(:disabled) {
|
||||
border-color: var(--overlay2);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
/* Add consistent spacing between input boxes */
|
||||
.input-box {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--surface0);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.input-box .label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--lavender);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Style select dropdown */
|
||||
select option {
|
||||
background-color: var(--base);
|
||||
color: var(--text);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,128 +1,93 @@
|
||||
<script>
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
import { t } from "svelte-i18n";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const modal = (/** @type {Element} */ node, { duration = 300 } = {}) => {
|
||||
const transform = getComputedStyle(node).transform;
|
||||
|
||||
const modal = (/** @type {Element} */ node, { duration = 300 } = {}) => {
|
||||
const transform = getComputedStyle(node).transform;
|
||||
|
||||
return {
|
||||
duration,
|
||||
easing: quintOut,
|
||||
css: (/** @type {any} */ t, /** @type {number} */ u) => {
|
||||
return `transform:
|
||||
return {
|
||||
duration,
|
||||
easing: quintOut,
|
||||
css: (/** @type {any} */ t, /** @type {number} */ u) => {
|
||||
return `transform:
|
||||
${transform}
|
||||
scale(${t})
|
||||
translateY(${u * -100}%)
|
||||
`;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function closeModal() {
|
||||
dispatch("close", {});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="modal-background">
|
||||
<div
|
||||
transition:modal={{ duration: 1000 }}
|
||||
class="modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
title={$t("cancel")}
|
||||
class="modal-close"
|
||||
on:click={closeModal}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keydown={(e) => e.key == "Enter" && closeModal()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 384 512"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">{$t("cancel")}</span>
|
||||
</a>
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div transition:modal|global={{ duration: 1000 }} class="modal" role="dialog" aria-modal="true">
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
}
|
||||
.modal-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--modal-backdrop); /* var(--base) with 0.75 opacity */
|
||||
backdrop-filter: blur(4px); /* Optional: adds a slight blur effect */
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 70%;
|
||||
box-shadow: 0 0 10px hsl(0 0% 0% / 10%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
@media (max-width: 990px) {
|
||||
.modal {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
.modal-close {
|
||||
border: none;
|
||||
}
|
||||
.modal {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 70%;
|
||||
background-color: var(--base);
|
||||
border: 1px solid var(--surface0);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(17, 17, 27, 0.5); /* var(--crust) with opacity */
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@media (max-width: 990px) {
|
||||
.modal {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
.modal .container {
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
background-color: var(--base);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-close svg {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
fill: rgb(14 165 233 /1);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
.modal-close:hover svg {
|
||||
fill: rgb(225 29 72);
|
||||
transform: scale(1.5);
|
||||
}
|
||||
.modal .container {
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
.modal .container {
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
/* Scrollbar styling */
|
||||
.modal .container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal .container::-webkit-scrollbar-track {
|
||||
background: var(--surface0);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal .container::-webkit-scrollbar-thumb {
|
||||
background: var(--surface2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal .container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--surface1);
|
||||
}
|
||||
|
||||
@media (min-width: 680px) {
|
||||
.modal .container {
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script>
|
||||
/** @type {number | null} */
|
||||
export let width;
|
||||
/** @type {string | null} */
|
||||
export let message;
|
||||
/** @type {number | null} */
|
||||
export let width;
|
||||
/** @type {string | null} */
|
||||
export let message;
|
||||
</script>
|
||||
|
||||
<div class="loading">
|
||||
<p class="simple-loader" style={width ? `width: ${width}px` : ""} />
|
||||
{#if message}
|
||||
<p>{message}</p>
|
||||
{/if}
|
||||
<p class="simple-loader" style={width ? `width: ${width}px` : ''}></p>
|
||||
{#if message}
|
||||
<p>{message}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading p {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading p {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
168
frontend/src/lib/components/SubscriptionEditForm.svelte
Normal file
168
frontend/src/lib/components/SubscriptionEditForm.svelte
Normal file
@@ -0,0 +1,168 @@
|
||||
<script>
|
||||
import InputField from '$lib/components/InputField.svelte';
|
||||
import SmallLoader from '$lib/components/SmallLoader.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { receive, send } from '$lib/utils/helpers';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { defaultSubscription } from '$lib/utils/defaults';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/** @type {App.Locals['user'] } */
|
||||
export let user;
|
||||
|
||||
/** @type {App.Types['subscription'] | null} */
|
||||
export let subscription;
|
||||
|
||||
console.log('Opening subscription modal with:', subscription);
|
||||
$: subscription = subscription || { ...defaultSubscription() };
|
||||
$: isLoading = subscription === undefined || user === undefined;
|
||||
let isUpdating = false;
|
||||
|
||||
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
|
||||
const handleUpdate = async () => {
|
||||
isUpdating = true;
|
||||
return async ({ result }) => {
|
||||
isUpdating = false;
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
dispatch('close');
|
||||
} else {
|
||||
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
<SmallLoader width={30} message={$t('loading.subscription_data')} />
|
||||
{:else if user && subscription}
|
||||
<form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}>
|
||||
<input name="susbscription[id]" type="hidden" bind:value={subscription.id} />
|
||||
<h1 class="step-title" style="text-align: center;">
|
||||
{subscription.id ? $t('subscriptions.edit') : $t('subscriptions.create')}
|
||||
</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive|global={{ key: error.id }}
|
||||
out:send|global={{ key: error.id }}
|
||||
>
|
||||
{$t(error.field) + ': ' + $t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="tab-content" style="display: block">
|
||||
<InputField
|
||||
name="subscription[name]"
|
||||
label={$t('subscriptions.name')}
|
||||
bind:value={subscription.name}
|
||||
placeholder={$t('placeholder.subscription_name')}
|
||||
required={true}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
<InputField
|
||||
name="subscription[details]"
|
||||
label={$t('details')}
|
||||
type="textarea"
|
||||
bind:value={subscription.details}
|
||||
placeholder={$t('placeholder.subscription_details')}
|
||||
required={true}
|
||||
/>
|
||||
<InputField
|
||||
name="subscription[conditions]"
|
||||
type="textarea"
|
||||
label={$t('subscriptions.conditions')}
|
||||
bind:value={subscription.conditions}
|
||||
placeholder={$t('placeholder.subscription_conditions')}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
<InputField
|
||||
name="subscription[monthly_fee]"
|
||||
type="number"
|
||||
label={$t('subscriptions.monthly_fee')}
|
||||
bind:value={subscription.monthly_fee}
|
||||
placeholder={$t('placeholder.subscription_monthly_fee')}
|
||||
required={true}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
<InputField
|
||||
name="subscription[hourly_rate]"
|
||||
type="number"
|
||||
label={$t('subscriptions.hourly_rate')}
|
||||
bind:value={subscription.hourly_rate}
|
||||
required={true}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
<InputField
|
||||
name="subscription[included_hours_per_year]"
|
||||
type="number"
|
||||
label={$t('subscriptions.included_hours_per_year')}
|
||||
bind:value={subscription.included_hours_per_year}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
<InputField
|
||||
name="included_hours_per_month"
|
||||
type="number"
|
||||
label={$t('subscriptions.included_hours_per_month')}
|
||||
bind:value={subscription.included_hours_per_month}
|
||||
readonly={subscription.id > 0}
|
||||
/>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
{#if isUpdating}
|
||||
<SmallLoader width={30} message={$t('loading.updating')} />
|
||||
{:else}
|
||||
<button type="button" class="button-dark" on:click={() => dispatch('cancel')}>
|
||||
{$t('cancel')}</button
|
||||
>
|
||||
<button type="submit" class="button-dark">{$t('confirm')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background-color: var(--surface0);
|
||||
border: 1px solid var(--surface1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container button {
|
||||
flex: 1 1 0;
|
||||
min-width: 120px;
|
||||
max-width: calc(50% - 5px);
|
||||
background-color: var(--surface1);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--overlay0);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.button-container button:hover {
|
||||
background-color: var(--surface2);
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.button-container button {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
</script>
|
||||
|
||||
{#key key}
|
||||
<div in:slide={{ duration, delay: duration }} out:slide={{ duration }}>
|
||||
<div in:slide|global={{ duration, delay: duration }} out:slide|global={{ duration }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
151
frontend/src/lib/css/bootstrap-custom.scss
vendored
Normal file
151
frontend/src/lib/css/bootstrap-custom.scss
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
// Core variables
|
||||
$theme-colors: (
|
||||
"primary": #d43aff,
|
||||
"secondary": #595b5c,
|
||||
"success": #00b7ef,
|
||||
"warning": rgb(225 29 72),
|
||||
"danger": #eb5424,
|
||||
"light": #9b9b9b,
|
||||
"dark": #2f2f2f,
|
||||
);
|
||||
|
||||
// Typography
|
||||
$font-family-base: "Quicksand", sans-serif;
|
||||
$font-family-monospace: "Roboto Mono", monospace;
|
||||
$font-size-base: 1rem;
|
||||
$line-height-base: 1.8;
|
||||
$headings-font-weight: normal;
|
||||
$headings-color: #fff;
|
||||
|
||||
// Body
|
||||
$body-bg: black;
|
||||
$body-color: #9b9b9b;
|
||||
|
||||
// Links
|
||||
$link-color: #00b7ef;
|
||||
$link-decoration: none;
|
||||
$link-hover-decoration: underline;
|
||||
|
||||
// Buttons
|
||||
$btn-padding-y: 1.125rem; // 18px
|
||||
$btn-padding-x: 1.75rem; // 28px
|
||||
$btn-font-weight: 500;
|
||||
$btn-letter-spacing: 1px;
|
||||
$btn-border-width: 1px;
|
||||
$btn-transition: all 0.3s ease-in-out;
|
||||
|
||||
// Forms
|
||||
$input-bg: #494848;
|
||||
$input-color: white;
|
||||
$input-border-color: #494848;
|
||||
$input-border-radius: 6px;
|
||||
$input-padding-y: 0.625rem; // 10px
|
||||
$input-padding-x: 0.625rem; // 10px
|
||||
$input-font-family: $font-family-monospace;
|
||||
$input-font-size: 1rem;
|
||||
$input-focus-border-color: lighten($input-border-color, 10%);
|
||||
$input-focus-box-shadow: none;
|
||||
|
||||
// Cards
|
||||
$card-bg: #2f2f2f;
|
||||
$card-border-radius: 3px;
|
||||
$card-border-width: 0;
|
||||
$card-spacer-y: 1.25rem;
|
||||
$card-spacer-x: 1.25rem;
|
||||
|
||||
// Modals
|
||||
$modal-content-bg: #2f2f2f;
|
||||
$modal-header-border-color: #595b5c;
|
||||
$modal-footer-border-color: #595b5c;
|
||||
|
||||
// Navbar
|
||||
$navbar-dark-color: #9b9b9b;
|
||||
$navbar-dark-hover-color: #fff;
|
||||
$navbar-padding-y: 1rem;
|
||||
$navbar-nav-link-padding-x: 1rem;
|
||||
|
||||
// Utilities
|
||||
$border-radius: 3px;
|
||||
$border-radius-lg: 6px;
|
||||
$box-shadow: none;
|
||||
|
||||
// Custom utility classes
|
||||
.text-uppercase {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
// Custom button styles
|
||||
.btn {
|
||||
text-transform: uppercase;
|
||||
|
||||
&-dark {
|
||||
@include button-variant(transparent, #595b5c);
|
||||
|
||||
&:hover {
|
||||
border-color: #fff;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&-primary {
|
||||
&:hover {
|
||||
background-color: darken(#d43aff, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom input styles
|
||||
.form-control {
|
||||
&:focus {
|
||||
background-color: lighten($input-bg, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 1200px;
|
||||
margin: 5em auto 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 45px 0;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 2rem 0;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 45px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0 32px;
|
||||
}
|
||||
|
||||
li strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
display: inline;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// Override Bootstrap's link hover behavior
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
border-bottom-color: #00b7ef;
|
||||
}
|
||||
// Import Bootstrap after variable overrides
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
762
frontend/src/lib/css/styles.min.css
vendored
762
frontend/src/lib/css/styles.min.css
vendored
@@ -1,46 +1,144 @@
|
||||
:root {
|
||||
--white: #ffffff;
|
||||
--black: #000000;
|
||||
--rosewater: #f5e0dc;
|
||||
--flamingo: #f2cdcd;
|
||||
--pink: #f5c2e7;
|
||||
--mauve: #cba6f7;
|
||||
--red: #f38ba8;
|
||||
--maroon: #eba0ac;
|
||||
--peach: #fab387;
|
||||
--yellow: #f9e2af;
|
||||
--light-green: #b5e8b0;
|
||||
--green: #3a8f46;
|
||||
--teal: #94e2d5;
|
||||
--sky: #89dceb;
|
||||
--sapphire: #74c7ec;
|
||||
--blue: #89b4fa;
|
||||
--lavender: #b4befe;
|
||||
--text: #cdd6f4;
|
||||
--subtext1: #bac2de;
|
||||
--subtext0: #a6adc8;
|
||||
--overlay2: #9399b2;
|
||||
--overlay1: #7f849c;
|
||||
--overlay0: #6c7086;
|
||||
--surface2: #585b70;
|
||||
--surface1: #45475a;
|
||||
--surface0: #313244;
|
||||
--base: #1e1e2e;
|
||||
--mantle: #181825;
|
||||
--crust: #11111b;
|
||||
--modal-backdrop: rgba(49, 50, 68, 0.45); /* For Mocha theme */
|
||||
|
||||
/* Bright theme (Latte) colors */
|
||||
--bright-white: #000000;
|
||||
--bright-black: #ffffff;
|
||||
--bright-rosewater: #dc8a78;
|
||||
--bright-flamingo: #dd7878;
|
||||
--bright-pink: #ea76cb;
|
||||
--bright-mauve: #8839ef;
|
||||
--bright-red: #d20f39;
|
||||
--bright-maroon: #e64553;
|
||||
--bright-peach: #fe640b;
|
||||
--bright-yellow: #df8e1d;
|
||||
--bright-light-green: #52b05d;
|
||||
--bright-green: #1b9200;
|
||||
--bright-teal: #179299;
|
||||
--bright-sky: #04a5e5;
|
||||
--bright-sapphire: #209fb5;
|
||||
--bright-blue: #1e66f5;
|
||||
--bright-lavender: #7287fd;
|
||||
--bright-text: #4c4f69;
|
||||
--bright-subtext1: #5c5f77;
|
||||
--bright-subtext0: #6c6f85;
|
||||
--bright-overlay2: #7c7f93;
|
||||
--bright-overlay1: #8c8fa1;
|
||||
--bright-overlay0: #9ca0b0;
|
||||
--bright-surface2: #acb0be;
|
||||
--bright-surface1: #bcc0cc;
|
||||
--bright-surface0: #ccd0da;
|
||||
--bright-base: #eff1f5;
|
||||
--bright-mantle: #e6e9ef;
|
||||
--bright-crust: #dce0e8;
|
||||
--bright-modal-backdrop: rgba(220, 224, 232, 0.45);
|
||||
}
|
||||
|
||||
[data-theme='bright'] {
|
||||
--white: var(--bright-white);
|
||||
--black: var(--bright-black);
|
||||
--rosewater: var(--bright-rosewater);
|
||||
--flamingo: var(--bright-flamingo);
|
||||
--pink: var(--bright-pink);
|
||||
--mauve: var(--bright-mauve);
|
||||
--red: var(--bright-red);
|
||||
--maroon: var(--bright-maroon);
|
||||
--peach: var(--bright-peach);
|
||||
--yellow: var(--bright-yellow);
|
||||
--light-green: var(--bright-light-green);
|
||||
--green: var(--bright-green);
|
||||
--teal: var(--bright-teal);
|
||||
--sky: var(--bright-sky);
|
||||
--sapphire: var(--bright-sapphire);
|
||||
--blue: var(--bright-blue);
|
||||
--lavender: var(--bright-lavender);
|
||||
--text: var(--bright-text);
|
||||
--subtext1: var(--bright-subtext1);
|
||||
--subtext0: var(--bright-subtext0);
|
||||
--overlay2: var(--bright-overlay2);
|
||||
--overlay1: var(--bright-overlay1);
|
||||
--overlay0: var(--bright-overlay0);
|
||||
--surface2: var(--bright-surface2);
|
||||
--surface1: var(--bright-surface1);
|
||||
--surface0: var(--bright-surface0);
|
||||
--base: var(--bright-base);
|
||||
--mantle: var(--bright-mantle);
|
||||
--crust: var(--bright-crust);
|
||||
--modal-backdrop: var(--bright-modal-backdrop);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Mono";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_gPq_ROW9.ttf)
|
||||
format("truetype");
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_gPq_ROW9.ttf)
|
||||
format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Roboto Mono";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW9.ttf)
|
||||
format("truetype");
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW9.ttf)
|
||||
format('truetype');
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 0 30px;
|
||||
background-color: black;
|
||||
color: #9b9b9b;
|
||||
font-family: "Quicksand", sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
padding: 0 30px;
|
||||
background-color: var(--base);
|
||||
color: var(--text);
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
}
|
||||
body {
|
||||
max-width: 1200px;
|
||||
margin: 5em auto 0 auto;
|
||||
max-width: 1200px;
|
||||
margin: 5em auto 0 auto;
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
display: inline;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
font-size: 16px;
|
||||
display: inline;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
color: white;
|
||||
border-style: none;
|
||||
height: 21px;
|
||||
font-size: 16px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
color: var(--text);
|
||||
border-style: none;
|
||||
height: 21px;
|
||||
font-size: 16px;
|
||||
}
|
||||
button {
|
||||
font-size: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
@@ -48,466 +146,408 @@ h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
h2 {
|
||||
margin: 0 0 45px 0;
|
||||
color: #fff;
|
||||
font-size: 36px;
|
||||
margin: 0 0 45px 0;
|
||||
color: var(--lavender);
|
||||
font-size: 36px;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 2rem 0;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
margin: 0 0 2rem 0;
|
||||
color: var(--lavender);
|
||||
font-size: 32px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 45px;
|
||||
line-height: 1.8;
|
||||
margin: 0 0 45px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
ul {
|
||||
margin: 0 0 32px;
|
||||
margin: 0 0 32px;
|
||||
}
|
||||
a {
|
||||
transition: border 0.2s ease-in-out;
|
||||
border-bottom: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
color: #00b7ef;
|
||||
transition: border 0.2s ease-in-out;
|
||||
border-bottom: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
color: var(--blue);
|
||||
}
|
||||
a:hover {
|
||||
border-bottom-color: #00b7ef;
|
||||
border-bottom-color: var(--blue);
|
||||
}
|
||||
li {
|
||||
line-height: 1.8;
|
||||
line-height: 1.8;
|
||||
}
|
||||
li strong {
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
margin: 0 0 32px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
margin: 0 0 32px;
|
||||
padding: 0;
|
||||
}
|
||||
.image img {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.optanon-alert-box-wrapper {
|
||||
left: 0;
|
||||
left: 0;
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
body {
|
||||
margin: 8em auto 0 auto;
|
||||
}
|
||||
.hide-mobile {
|
||||
display: initial;
|
||||
}
|
||||
body {
|
||||
margin: 8em auto 0 auto;
|
||||
}
|
||||
.hide-mobile {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 3em 0 0;
|
||||
background: black;
|
||||
}
|
||||
.header.top-banner-open {
|
||||
margin-top: 5px;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.header .header-container {
|
||||
width: 100%;
|
||||
max-width: calc(1200px + 10em);
|
||||
height: 5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header .header-container .header-left {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container a {
|
||||
display: flex;
|
||||
color: #9b9b9b;
|
||||
border: none;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container a img {
|
||||
height: 28px;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container a span {
|
||||
display: inline-block;
|
||||
margin: 2px 1ch 0 0;
|
||||
}
|
||||
.header .header-container .header-left .header-crafted-by-container .auth0 {
|
||||
margin-left: 1ch;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.header .header-container .header-right {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item {
|
||||
text-transform: uppercase;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item.active a,
|
||||
.header .header-container .header-right .header-nav-item.active button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header .header-container .header-right a img {
|
||||
margin-top: -0.4rem;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.header .header-container .header-right .header-nav-item a,
|
||||
.header .header-container .header-right .header-nav-item button {
|
||||
transition: color 0.3s ease-in-out;
|
||||
display: block;
|
||||
padding: 20px 0;
|
||||
border: none;
|
||||
color: #9b9b9b;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item:hover a,
|
||||
.header .header-container .header-right .header-nav-item:hover button {
|
||||
color: #fdfff5;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
.header {
|
||||
padding: 3em 5rem 0;
|
||||
}
|
||||
.header.top-banner-open {
|
||||
margin-top: 48px;
|
||||
}
|
||||
.header .header-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
.header .header-container .header-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.header .header-container .header-right .header-nav-item {
|
||||
margin-left: 26px;
|
||||
}
|
||||
}
|
||||
.button-dark {
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid #595b5c;
|
||||
margin: 2px;
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
background-color 0.3s ease-in-out;
|
||||
color: var(--white);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--surface1);
|
||||
margin: 2px;
|
||||
}
|
||||
.button-dark:hover {
|
||||
border-color: #fff;
|
||||
border-color: var(--text);
|
||||
}
|
||||
.button-colorful {
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: #d43aff;
|
||||
border: 1px solid #d43aff;
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
background-color 0.3s ease-in-out;
|
||||
color: var(--white);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: var(--mauve);
|
||||
border: 1px solid var(--mauve);
|
||||
}
|
||||
.button-colorful:hover {
|
||||
background-color: #c907ff;
|
||||
border-color: #c907ff;
|
||||
background-color: var(--bright-mauve);
|
||||
border-color: var(--bright-mauve);
|
||||
}
|
||||
.button-orange {
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: #eb5424;
|
||||
border: 1px solid #eb5424;
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
background-color 0.3s ease-in-out;
|
||||
color: var(--white);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: var(--peach);
|
||||
border: 1px solid var(--peach);
|
||||
}
|
||||
.button-orange:hover {
|
||||
background-color: #ca3f12;
|
||||
border-color: #ca3f12;
|
||||
background-color: var(--bright-peach);
|
||||
border-color: var(--bright-peach);
|
||||
}
|
||||
.button-colorful:disabled {
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: #9a9a9a;
|
||||
border: 1px solid #9a9a9a;
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
background-color 0.3s ease-in-out;
|
||||
color: var(--white);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
padding: 18px 28px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
background-color: var(--overlay0);
|
||||
border: 1px solid var(--overlay0);
|
||||
}
|
||||
.hero-container {
|
||||
max-width: 795px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 auto 70px auto;
|
||||
max-width: 795px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 auto 70px auto;
|
||||
}
|
||||
.hero-container .hero-logo {
|
||||
margin-top: 88px;
|
||||
margin-bottom: 32px;
|
||||
margin-top: 88px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.hero-container .hero-subtitle {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
margin: 0 0 45px 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
margin: 0 0 45px 0;
|
||||
}
|
||||
.hero-container .hero-buttons-container {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
.hero-container .hero-buttons-container button {
|
||||
margin: 0 8px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
.hero-container {
|
||||
margin: 0 auto 140px auto;
|
||||
}
|
||||
.hero-container {
|
||||
margin: 0 auto 140px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
color: white;
|
||||
letter-spacing: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
color: var(--white);
|
||||
letter-spacing: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.container .content {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.container .content .step-title {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 86px;
|
||||
opacity: 1;
|
||||
color: var(--white);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 86px;
|
||||
opacity: 1;
|
||||
}
|
||||
.container .content .step-subtitle {
|
||||
position: relative;
|
||||
top: -5px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.container .content {
|
||||
margin-top: 120px;
|
||||
}
|
||||
.container .content {
|
||||
margin-top: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 680px) {
|
||||
.container {
|
||||
position: relative;
|
||||
left: 100px;
|
||||
display: flex;
|
||||
width: calc(100% - 100px);
|
||||
padding: 0;
|
||||
}
|
||||
.container .content {
|
||||
max-width: 795px;
|
||||
}
|
||||
.container .content .step-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
left: 100px;
|
||||
display: flex;
|
||||
width: calc(100% - 100px);
|
||||
padding: 0;
|
||||
}
|
||||
.container .content {
|
||||
max-width: 795px;
|
||||
}
|
||||
.container .content .step-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
.input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: #2f2f2f;
|
||||
border-radius: 3px;
|
||||
font-family: "Roboto Mono", monospace;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--surface0);
|
||||
border-radius: 3px;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.input-box .label {
|
||||
margin: 0 1ch 0 0;
|
||||
font-size: 16px;
|
||||
margin: 0 1ch 0 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.input-box .input {
|
||||
background-color: #494848;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
border: 3px solid #494848;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
font-size: 13px;
|
||||
background-color: var(--surface1);
|
||||
border: 3px solid var(--surface1);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
font-size: 13px;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
.input-box {
|
||||
padding: 10px;
|
||||
}
|
||||
.input-box {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: first baseline;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: first baseline;
|
||||
}
|
||||
@media (max-width: 680px) {
|
||||
.btn-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.btn-container p {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.btn-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.btn-container p {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
letter-spacing: 1px;
|
||||
padding: 18px 28px;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
background-color 0.3s ease-in-out;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background-color: var(--blue);
|
||||
border-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background-color: var(--sapphire);
|
||||
border-color: var(--sapphire);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background-color: var(--red);
|
||||
border-color: var(--red);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background-color: var(--maroon);
|
||||
border-color: var(--maroon);
|
||||
}
|
||||
.warning {
|
||||
margin: 20px 0;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(255 228 230);
|
||||
border: 1px solid rgb(225 29 72);
|
||||
border-radius: 6px;
|
||||
color: rgb(225 29 72);
|
||||
font-size: 16px;
|
||||
margin: 20px 0;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--surface0);
|
||||
border: 1px solid var(--red);
|
||||
color: var(--red);
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.warning a {
|
||||
color: rgb(225 29 72);
|
||||
text-decoration: underline;
|
||||
color: var(--red);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.warning.hidden {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 10rem;
|
||||
padding: 30px 40px;
|
||||
background: #2f3132;
|
||||
color: #fff;
|
||||
margin-top: 10rem;
|
||||
padding: 30px 40px;
|
||||
background: var(--surface0);
|
||||
color: var(--text);
|
||||
}
|
||||
.error p {
|
||||
margin: 0 0 1rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
.error p.intro {
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.error .button-colorful {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
}
|
||||
@media (min-width: 680px) {
|
||||
.error {
|
||||
padding: 65px 80px;
|
||||
}
|
||||
.error {
|
||||
padding: 65px 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-branding-container {
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
margin-bottom: 73px;
|
||||
color: var(--white);
|
||||
font-weight: 300;
|
||||
margin-bottom: 73px;
|
||||
}
|
||||
|
||||
.footer-branding-container .footer-branding {
|
||||
display: flex;
|
||||
width: 400px;
|
||||
display: flex;
|
||||
width: 400px;
|
||||
}
|
||||
.footer-branding-container .footer-branding {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
margin: 30px 0 0;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
margin: 30px 0 0;
|
||||
}
|
||||
.footer-branding-container .footer-branding .footer-crafted-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--white);
|
||||
}
|
||||
.footer-branding-container .footer-branding .footer-crafted-by-container span {
|
||||
display: inline-block;
|
||||
margin: 3px 1ch 0 0;
|
||||
display: inline-block;
|
||||
margin: 3px 1ch 0 0;
|
||||
}
|
||||
.footer-branding-container
|
||||
.footer-branding
|
||||
.footer-crafted-by-container
|
||||
.footer-branded-crafted-img {
|
||||
height: 28px;
|
||||
.footer-branding
|
||||
.footer-crafted-by-container
|
||||
.footer-branded-crafted-img {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.footer-branding-container .footer-branding .footer-copyright {
|
||||
color: #696969;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--overlay0);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.footer-container {
|
||||
width: 100%;
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--white);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) {
|
||||
.footer-container {
|
||||
padding: 0;
|
||||
}
|
||||
.footer-container {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simple-loader {
|
||||
--b: 20px; /* border thickness */
|
||||
--n: 15; /* number of dashes*/
|
||||
--g: 7deg; /* gap between dashes*/
|
||||
--c: #d43aff; /* the color */
|
||||
--b: 20px; /* border thickness */
|
||||
--n: 15; /* number of dashes*/
|
||||
--g: 7deg; /* gap between dashes*/
|
||||
--c: var(--mauve); /* Changed loader color to match theme */
|
||||
|
||||
width: 40px; /* size */
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
padding: 1px; /* get rid of bad outlines */
|
||||
background: conic-gradient(#0000, var(--c)) content-box;
|
||||
--_m: /* we use +/-1deg between colors to avoid jagged edges */ repeating-conic-gradient(
|
||||
#0000 0deg,
|
||||
#000 1deg calc(360deg / var(--n) - var(--g) - 1deg),
|
||||
#0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n))
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side,
|
||||
#0000 calc(98% - var(--b)),
|
||||
#000 calc(100% - var(--b))
|
||||
);
|
||||
-webkit-mask: var(--_m);
|
||||
mask: var(--_m);
|
||||
-webkit-mask-composite: destination-in;
|
||||
mask-composite: intersect;
|
||||
animation: load 1s infinite steps(var(--n));
|
||||
width: 40px; /* size */
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
padding: 1px; /* get rid of bad outlines */
|
||||
background: conic-gradient(#0000, var(--c)) content-box;
|
||||
--_m: /* we use +/-1deg between colors to avoid jagged edges */ repeating-conic-gradient(
|
||||
#0000 0deg,
|
||||
#000 1deg calc(360deg / var(--n) - var(--g) - 1deg),
|
||||
#0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n))
|
||||
),
|
||||
radial-gradient(farthest-side, #0000 calc(98% - var(--b)), var(--black) calc(100% - var(--b)));
|
||||
-webkit-mask: var(--_m);
|
||||
mask: var(--_m);
|
||||
-webkit-mask-composite: destination-in;
|
||||
mask-composite: intersect;
|
||||
animation: load 1s infinite steps(var(--n));
|
||||
}
|
||||
@keyframes load {
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +1,264 @@
|
||||
export default {
|
||||
userStatus: {
|
||||
1: "Nicht verifiziert",
|
||||
2: "Verifiziert",
|
||||
3: "Aktiv",
|
||||
4: "Passiv",
|
||||
5: "Deaktiviert",
|
||||
},
|
||||
userRole: {
|
||||
0: "Mitglied",
|
||||
1: "Betrachter",
|
||||
4: "Bearbeiter",
|
||||
8: "Administrator",
|
||||
},
|
||||
placeholder: {
|
||||
password: "Passwort eingeben...",
|
||||
email: "Emailadresse eingeben...",
|
||||
company: "Firmennamen eingeben...",
|
||||
first_name: "Vornamen eingeben...",
|
||||
last_name: "Nachnamen eingeben...",
|
||||
phone: "Telefonnummer eingeben...",
|
||||
address: "Straße und Hausnummer eingeben...",
|
||||
zip_code: "Postleitzahl eingeben...",
|
||||
city: "Wohnort eingeben...",
|
||||
bank_name: "Namen der Bank eingeben...",
|
||||
parent_member_id: "Mitgliedsnr des Hauptmitglieds eingeben...",
|
||||
bank_account_holder: "Namen eingeben...",
|
||||
iban: "IBAN eingeben..",
|
||||
bic: "BIC eingeben(Bei nicht deutschen Konten)...",
|
||||
mandate_reference: "SEPA Mandatsreferenz eingeben..",
|
||||
notes: "Deine Notizen zu {name}...",
|
||||
licence_number: "Auf dem Führerschein unter Feld 5",
|
||||
issued_date: "Ausgabedatum unter Feld 4a",
|
||||
expiration_date: "Ablaufdatum unter Feld 4b",
|
||||
issuing_country: "Ausstellendes Land",
|
||||
},
|
||||
validation: {
|
||||
required: "Eingabe benötigt",
|
||||
password: "Password zu kurz, mindestens 8 Zeichen",
|
||||
password_match: "Passwörter stimmen nicht überein!",
|
||||
phone: "Ungültiges Format(+491738762387 oder 0173850698)",
|
||||
zip_code: "Ungültige Postleitzahl(Nur deutsche Wohnorte sind zulässig)",
|
||||
bic: "Ungültige BIC",
|
||||
iban: "Ungültige IBAN",
|
||||
date: "Bitte geben Sie ein Datum ein",
|
||||
email: "Ungültige Emailadresse",
|
||||
licence: "Nummer zu kurz(11 Zeichen)",
|
||||
},
|
||||
server: {
|
||||
error: {
|
||||
invalid_json: "JSON Daten sind ungültig",
|
||||
no_auth_token: "Nicht authorisiert, fehlender oder ungültiger Auth-Token",
|
||||
jwt_parsing_error:
|
||||
"Nicht authorisiert, Auth-Token konnte nicht gelesen werden",
|
||||
unauthorized_update: "Sie sind nicht befugt dieses Update durchzuführen",
|
||||
internal_server_error:
|
||||
"Verdammt, fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie nach Hilfe",
|
||||
},
|
||||
validation: {
|
||||
no_user_id_provided: "Nutzer ID fehlt im Header",
|
||||
invalid_subscription_model: "Model nicht gefunden",
|
||||
user_not_found: "{field} konnte nicht gefunden werden",
|
||||
invalid_user_data: "Nutzerdaten ungültig",
|
||||
user_not_found_or_wrong_password:
|
||||
"Existiert nicht oder falsches Passwort",
|
||||
email_already_registered:
|
||||
"Ein Mitglied wurde schon mit dieser Emailadresse erstellt.",
|
||||
alphanumunicode: "beinhaltet nicht erlaubte Zeichen",
|
||||
safe_content: "I see what you did there! Do not cross this line!",
|
||||
iban: "Ungültig. Format: DE07123412341234123412",
|
||||
bic: "Ungültig. Format: BELADEBEXXX",
|
||||
email: "Format ungültig",
|
||||
number: "Ist keine Nummer",
|
||||
euDriversLicence: "Ist kein europäischer Führerschein",
|
||||
lte: "Ist zu groß/neu",
|
||||
gt: "Ist zu klein/alt",
|
||||
required: "Feld wird benötigt",
|
||||
image: "Dies ist kein Bild",
|
||||
alphanum: "beinhaltet ungültige Zeichen",
|
||||
alphaunicode: "darf nur aus Buchstaben bestehen",
|
||||
},
|
||||
},
|
||||
licenceCategory: {
|
||||
AM: "Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)",
|
||||
A1: "Leichte Motorräder (125ccm)",
|
||||
A2: "Motorräder mit mittlerer Leistung (max 35kW)",
|
||||
A: "Motorräder",
|
||||
B: "Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze",
|
||||
C1: "Mittelschwere Fahrzeuge -7500 kg",
|
||||
C: "Schwere Nutzfahrzeuge > 3500 kg",
|
||||
D1: "Kleinbusse 9-16 Sitzplätze",
|
||||
D: "Busse > 8 Sitzplätze",
|
||||
BE: "Fahrzeugklasse B mit Anhänger",
|
||||
C1E: "Fahrzeugklasse C1 mit Anhänger",
|
||||
CE: "Fahrzeugklasse C mit Anhänger",
|
||||
D1E: "Fahrzeugklasse D1 mit Anhänger",
|
||||
DE: "Fahrzeugklasse D mit Anhänger",
|
||||
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h",
|
||||
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h",
|
||||
},
|
||||
user: {
|
||||
login: "Nutzer Anmeldung",
|
||||
edit: "Nutzer bearbeiten",
|
||||
user: "Nutzer",
|
||||
management: "Mitgliederverwaltung",
|
||||
id: "Mitgliedsnr",
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
status: "Status",
|
||||
role: "Nutzerrolle",
|
||||
},
|
||||
cancel: "Abbrechen",
|
||||
confirm: "Bestätigen",
|
||||
actions: "Aktionen",
|
||||
edit: "Bearbeiten",
|
||||
delete: "Löschen",
|
||||
mandate_date_signed: "Mandatserteilungsdatum",
|
||||
licence_categories: "Führerscheinklassen",
|
||||
subscription_model: "Mitgliedschatfsmodell",
|
||||
licence: "Führerschein",
|
||||
licence_number: "Führerscheinnummer",
|
||||
issued_date: "Ausgabedatum",
|
||||
expiration_date: "Ablaufdatum",
|
||||
country: "Land",
|
||||
monthly_fee: "Monatliche Gebühr",
|
||||
hourly_rate: "Stundensatz",
|
||||
details: "Details",
|
||||
conditions: "Bedingungen",
|
||||
unknown: "Unbekannt",
|
||||
notes: "Notizen",
|
||||
address: "Straße & Hausnummer",
|
||||
city: "Wohnort",
|
||||
zip_code: "PLZ",
|
||||
forgot_password: "Passwort vergessen?",
|
||||
password: "Passwort",
|
||||
password_repeat: "Passwort wiederholen",
|
||||
email: "Email",
|
||||
company: "Firma",
|
||||
login: "Anmeldung",
|
||||
profile: "Profil",
|
||||
membership: "Mitgliedschaft",
|
||||
bankaccount: "Kontodaten",
|
||||
first_name: "Vorname",
|
||||
last_name: "Nachname",
|
||||
name: "Name",
|
||||
phone: "Telefonnummer",
|
||||
birth_date: "Geburtstag",
|
||||
status: "Status",
|
||||
start: "Beginn",
|
||||
end: "Ende",
|
||||
parent_member_id: "Hauptmitgliedsnr.",
|
||||
bank_account_holder: "Kontoinhaber",
|
||||
bank_name: "Bank",
|
||||
iban: "IBAN",
|
||||
bic: "BIC",
|
||||
mandate_reference: "SEPA Mandat",
|
||||
userStatus: {
|
||||
1: 'Nicht verifiziert',
|
||||
2: 'Deaktiviert',
|
||||
3: 'Verifiziert',
|
||||
4: 'Systemzugang',
|
||||
5: 'Passiv'
|
||||
},
|
||||
userRole: {
|
||||
'-1': 'Unfallgegner',
|
||||
0: 'Sponsor',
|
||||
1: 'Mitglied',
|
||||
2: 'Betrachter',
|
||||
4: 'Bearbeiter',
|
||||
8: 'Administrator'
|
||||
},
|
||||
placeholder: {
|
||||
car_name: 'Hat das Fahrzeug einen Namen?',
|
||||
car_brand: 'Fahrzeughersteller eingeben...',
|
||||
car_model: 'Fahrzeugmodell eingeben...',
|
||||
car_color: 'Fahrzeugfarbe eingeben...',
|
||||
car_licence_plate: 'Fahrzeugkennzeichen eingeben...',
|
||||
insurance_reference: 'Versicherungsnummer eingeben...',
|
||||
password: 'Passwort eingeben...',
|
||||
email: 'Emailadresse eingeben...',
|
||||
company: 'Firmennamen eingeben...',
|
||||
first_name: 'Vornamen eingeben...',
|
||||
last_name: 'Nachnamen eingeben...',
|
||||
phone: 'Telefonnummer eingeben...',
|
||||
address: 'Straße und Hausnummer eingeben...',
|
||||
zip_code: 'Postleitzahl eingeben...',
|
||||
city: 'Wohnort eingeben...',
|
||||
bank_name: 'Namen der Bank eingeben...',
|
||||
parent_member_id: 'Mitgliedsnr des Hauptmitglieds eingeben...',
|
||||
bank_account_holder: 'Namen eingeben...',
|
||||
iban: 'IBAN eingeben..',
|
||||
bic: 'BIC eingeben(Bei nicht deutschen Konten)...',
|
||||
mandate_reference: 'SEPA Mandatsreferenz eingeben..',
|
||||
notes: 'Deine Notizen zu {name}...',
|
||||
licence_number: 'Auf dem Führerschein unter Feld 5',
|
||||
issued_date: 'Ausgabedatum unter Feld 4a',
|
||||
expiration_date: 'Ablaufdatum unter Feld 4b',
|
||||
issuing_country: 'Ausstellendes Land',
|
||||
subscription_name: 'Name des Tarifmodells',
|
||||
subscription_details: 'Beschreibe das Tarifmodell...',
|
||||
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...',
|
||||
search: 'Suchen...'
|
||||
},
|
||||
validation: {
|
||||
required: 'Eingabe benötigt',
|
||||
password: 'Password zu kurz, mindestens 8 Zeichen',
|
||||
password_match: 'Passwörter stimmen nicht überein!',
|
||||
phone: 'Ungültiges Format(+491738762387 oder 0173850698)',
|
||||
zip_code: 'Ungültige Postleitzahl(Nur deutsche Wohnorte sind zulässig)',
|
||||
bic: 'Ungültige BIC',
|
||||
iban: 'Ungültige IBAN',
|
||||
date: 'Bitte geben Sie ein Datum ein',
|
||||
email: 'Ungültige Emailadresse',
|
||||
licence: 'Nummer zu kurz(11 Zeichen)'
|
||||
},
|
||||
server: {
|
||||
general: 'Allgemein',
|
||||
error: {
|
||||
invalid_json: 'JSON Daten sind ungültig',
|
||||
no_auth_token: 'Nicht authorisiert, fehlender oder ungültiger Auth-Token',
|
||||
jwt_parsing_error: 'Nicht authorisiert, Auth-Token konnte nicht gelesen werden',
|
||||
unauthorized: 'Sie sind nicht befugt diese Handlung durchzuführen',
|
||||
internal_server_error:
|
||||
'Verdammt, Fehler auf unserer Seite, probieren Sie es nochmal, danach rufen Sie jemanden vom Verein an.',
|
||||
not_possible: 'Vorgang nicht möglich.',
|
||||
not_found: 'Konnte nicht gefunden werden.',
|
||||
in_use: 'Ist in Benutzung',
|
||||
undelivered_verification_mail:
|
||||
'Registrierung erfolgreicht, leider konnte die Verifizierungs-E-Mail nicht versendet werden. Bitte wenden Sie sich an den Verein um Ihre Emailadresse zu bestätigen und Ihren Account zu aktivieren.'
|
||||
},
|
||||
validation: {
|
||||
invalid: 'ungültig',
|
||||
invalid_user_id: 'Nutzer ID ungültig',
|
||||
invalid_subscription: 'Model nicht gefunden',
|
||||
user_not_found: '{field} konnte nicht gefunden werden',
|
||||
invalid_user_data: 'Nutzerdaten ungültig',
|
||||
user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
|
||||
email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.',
|
||||
password_already_changed: 'Das Passwort wurde schon geändert.',
|
||||
user_already_verified: 'Ihre Email Adresse wurde schon bestätigt.',
|
||||
insecure: 'Unsicheres Passwort, versuchen Sie {message}',
|
||||
longer: 'oder verwenden Sie ein längeres Passwort',
|
||||
special: 'mehr Sonderzeichen einzufügen',
|
||||
lowercase: 'Kleinbuchstaben zu verwenden',
|
||||
uppercase: 'Großbuchstaben zu verwenden',
|
||||
numbers: 'Zahlen zu verwenden',
|
||||
alphanumunicode: 'beinhaltet nicht erlaubte Zeichen',
|
||||
safe_content: 'I see what you did there! Do not cross this line!',
|
||||
iban: 'Ungültig. Format: DE07123412341234123412',
|
||||
bic: 'Ungültig. Format: BELADEBEXXX',
|
||||
email: 'Format ungültig',
|
||||
number: 'Ist keine Nummer',
|
||||
euDriversLicence: 'Ist kein europäischer Führerschein',
|
||||
lte: 'Ist zu groß/neu',
|
||||
gt: 'Ist zu klein/alt',
|
||||
required: 'Feld wird benötigt',
|
||||
image: 'Dies ist kein Bild',
|
||||
alphanum: 'beinhaltet ungültige Zeichen',
|
||||
user_disabled: 'Benutzer ist deaktiviert',
|
||||
duplicate: 'Schon vorhanden..',
|
||||
alphaunicode: 'darf nur aus Buchstaben bestehen',
|
||||
too_soon: 'zu früh'
|
||||
}
|
||||
},
|
||||
licenceCategory: {
|
||||
AM: 'Mopeds und leichte vierrädrige Kraftfahrzeuge (50ccm, max 45km/h)',
|
||||
A1: 'Leichte Motorräder (125ccm)',
|
||||
A2: 'Motorräder mit mittlerer Leistung (max 35kW)',
|
||||
A: 'Motorräder',
|
||||
B: 'Kraftfahrzeuge ≤ 3500 kg, ≤ 8 Sitzplätze',
|
||||
C1: 'Mittelschwere Fahrzeuge -7500 kg',
|
||||
C: 'Schwere Nutzfahrzeuge > 3500 kg',
|
||||
D1: 'Kleinbusse 9-16 Sitzplätze',
|
||||
D: 'Busse > 8 Sitzplätze',
|
||||
BE: 'Fahrzeugklasse B mit Anhänger',
|
||||
C1E: 'Fahrzeugklasse C1 mit Anhänger',
|
||||
CE: 'Fahrzeugklasse C mit Anhänger',
|
||||
D1E: 'Fahrzeugklasse D1 mit Anhänger',
|
||||
DE: 'Fahrzeugklasse D mit Anhänger',
|
||||
L: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h',
|
||||
T: 'Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h'
|
||||
},
|
||||
users: 'Mitglieder',
|
||||
user: {
|
||||
login: 'Nutzer Anmeldung',
|
||||
edit: 'Nutzer bearbeiten',
|
||||
create: 'Nutzer erstellen',
|
||||
user: 'Nutzer',
|
||||
member: 'Mitglied',
|
||||
management: 'Mitgliederverwaltung',
|
||||
id: 'Mitgliedsnr',
|
||||
first_name: 'Vorname',
|
||||
last_name: 'Nachname',
|
||||
phone: 'Telefonnummer',
|
||||
dateofbirth: 'Geburtstag',
|
||||
email: 'Email',
|
||||
membership: 'Mitgliedschaft',
|
||||
bank_account: 'Kontodaten',
|
||||
status: 'Status',
|
||||
role: 'Nutzerrolle',
|
||||
supporter: 'Sponsor',
|
||||
opponent: 'Unfallgegner'
|
||||
},
|
||||
subscriptions: {
|
||||
name: 'Modellname',
|
||||
edit: 'Modell bearbeiten',
|
||||
create: 'Modell erstellen',
|
||||
subscription: 'Tarifmodell',
|
||||
subscriptions: 'Tarifmodelle',
|
||||
conditions: 'Bedingungen',
|
||||
monthly_fee: 'Monatliche Gebühr',
|
||||
hourly_rate: 'Stundensatz',
|
||||
included_hours_per_year: 'Inkludierte Stunden pro Jahr',
|
||||
included_hours_per_month: 'Inkludierte Stunden pro Monat'
|
||||
},
|
||||
car: {
|
||||
car: 'Fahrzeug',
|
||||
model: 'Modell',
|
||||
brand: 'Marke',
|
||||
licence_plate: 'Kennzeichen',
|
||||
edit: 'Fahrzeug bearbeiten',
|
||||
create: 'Fahrzeug hinzufügen',
|
||||
damages: 'Schäden',
|
||||
start_date: 'Anschaffungsdatum',
|
||||
end_date: 'Leasingende',
|
||||
leasing_rate: 'Leasingrate'
|
||||
},
|
||||
insurances: {
|
||||
edit: 'Daten bearbeiten',
|
||||
create: 'Versicherung erstellen'
|
||||
},
|
||||
loading: {
|
||||
user_data: 'Lade Nutzerdaten',
|
||||
subscription_data: 'Lade Modelldaten',
|
||||
insurance_data: 'Lade Versicherungsdaten',
|
||||
car_data: 'Lade Fahrzeugdaten',
|
||||
please_wait: 'Bitte warten...',
|
||||
updating: 'Aktualisiere...'
|
||||
},
|
||||
dialog: {
|
||||
user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?',
|
||||
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?',
|
||||
car_deletion: 'Soll das Fahrzeug {name} wirklich gelöscht werden?',
|
||||
insurance_deletion: 'Soll die Versicherung {name} wirklich gelöscht werden?',
|
||||
damage_deletion: 'Soll der Schaden {name} wirklich gelöscht werden?',
|
||||
backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?'
|
||||
},
|
||||
cancel: 'Abbrechen',
|
||||
confirm: 'Bestätigen',
|
||||
actions: 'Aktionen',
|
||||
create: 'Hinzufügen',
|
||||
edit: 'Bearbeiten',
|
||||
delete: 'Löschen',
|
||||
not_set: 'Nicht gesetzt',
|
||||
noone: 'Niemand',
|
||||
search: 'Suche:',
|
||||
name: 'Name',
|
||||
date: 'Datum',
|
||||
price: 'Preis',
|
||||
color: 'Farbe',
|
||||
grant_backend_access: 'Backend Zugriff gewähren',
|
||||
no_insurance: 'Keine Versicherung',
|
||||
supporter: 'Sponsoren',
|
||||
mandate_date_signed: 'Mandatserteilungsdatum',
|
||||
licence_categories: 'Führerscheinklassen',
|
||||
subscription: 'Mitgliedschatfsmodell',
|
||||
licence: 'Führerschein',
|
||||
licence_number: 'Führerscheinnummer',
|
||||
insurance: 'Versicherung',
|
||||
insurance_reference: 'Versicherungsnummer',
|
||||
issued_date: 'Ausgabedatum',
|
||||
month: 'Monat',
|
||||
expiration_date: 'Ablaufdatum',
|
||||
country: 'Land',
|
||||
details: 'Details',
|
||||
unknown: 'Unbekannt',
|
||||
notes: 'Notizen',
|
||||
address: 'Straße & Hausnummer',
|
||||
city: 'Wohnort',
|
||||
zip_code: 'PLZ',
|
||||
forgot_password: 'Passwort vergessen?',
|
||||
password: 'Passwort',
|
||||
confirm_password: 'Passwort wiederholen',
|
||||
password_changed: 'Passwort wurde erfolgreich geändert.',
|
||||
change_password: 'Passwort ändern',
|
||||
password_change_requested:
|
||||
'Passwortänderungsanfrage wurde gesendet.. Bitte überprüfen Sie Ihr Postfach.',
|
||||
company: 'Firma',
|
||||
login: 'Anmeldung',
|
||||
profile: 'Profil',
|
||||
cars: 'Fahrzeuge',
|
||||
status: 'Status',
|
||||
start: 'Beginn',
|
||||
end: 'Ende',
|
||||
parent_member_id: 'Hauptmitgliedsnr.',
|
||||
bank_account_holder: 'Kontoinhaber',
|
||||
bank_name: 'Bank',
|
||||
iban: 'IBAN',
|
||||
bic: 'BIC',
|
||||
mandate_reference: 'SEPA Mandat',
|
||||
payments: 'Zahlungen',
|
||||
add_new: 'Neu',
|
||||
email_sent: 'Email wurde gesendet..',
|
||||
verification: 'Verifikation',
|
||||
// For payments section
|
||||
payment: {
|
||||
id: 'Zahlungs-Nr',
|
||||
amount: 'Betrag',
|
||||
date: 'Datum',
|
||||
status: 'Status'
|
||||
},
|
||||
// For subscription statuses
|
||||
subscriptionStatus: {
|
||||
pending: 'Ausstehend',
|
||||
completed: 'Abgeschlossen',
|
||||
failed: 'Fehlgeschlagen',
|
||||
cancelled: 'Storniert'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,210 @@
|
||||
export default {
|
||||
userStatus: {
|
||||
1: "Unverified",
|
||||
2: "Verified",
|
||||
3: "Active",
|
||||
4: "Passive",
|
||||
5: "Disabled",
|
||||
},
|
||||
userRole: {
|
||||
0: "Member",
|
||||
1: "Viewer",
|
||||
4: "Editor",
|
||||
8: "Admin",
|
||||
},
|
||||
userStatus: {
|
||||
1: 'Not Verified',
|
||||
2: 'Deactivated',
|
||||
3: 'Verified',
|
||||
4: 'System Access',
|
||||
5: 'Passive'
|
||||
},
|
||||
userRole: {
|
||||
0: 'Sponsor',
|
||||
1: 'Member',
|
||||
2: 'Viewer',
|
||||
4: 'Editor',
|
||||
8: 'Administrator'
|
||||
},
|
||||
placeholder: {
|
||||
password: 'Enter password...',
|
||||
email: 'Enter email address...',
|
||||
company: 'Enter company name...',
|
||||
first_name: 'Enter first name...',
|
||||
last_name: 'Enter last name...',
|
||||
phone: 'Enter phone number...',
|
||||
address: 'Enter street and house number...',
|
||||
zip_code: 'Enter postal code...',
|
||||
city: 'Enter city...',
|
||||
bank_name: 'Enter bank name...',
|
||||
parent_member_id: 'Enter parent member ID...',
|
||||
bank_account_holder: 'Enter name...',
|
||||
iban: 'Enter IBAN...',
|
||||
bic: 'Enter BIC (for non-German accounts)...',
|
||||
mandate_reference: 'Enter SEPA mandate reference...',
|
||||
notes: 'Your notes about {name}...',
|
||||
licence_number: 'On the driver’s licence under field 5',
|
||||
issued_date: 'Issue date under field 4a',
|
||||
expiration_date: 'Expiration date under field 4b',
|
||||
issuing_country: 'Issuing country',
|
||||
subscription_name: 'Subscription model name',
|
||||
subscription_details: 'Describe the subscription model...',
|
||||
subscription_conditions: 'Describe the usage conditions...',
|
||||
search: 'Search...'
|
||||
},
|
||||
validation: {
|
||||
required: 'Input required',
|
||||
password: 'Password too short, at least 8 characters',
|
||||
password_match: 'Passwords do not match!',
|
||||
phone: 'Invalid format (+491738762387 or 0173850698)',
|
||||
zip_code: 'Invalid postal code (Only German locations are allowed)',
|
||||
bic: 'Invalid BIC',
|
||||
iban: 'Invalid IBAN',
|
||||
date: 'Please enter a date',
|
||||
email: 'Invalid email address',
|
||||
licence: 'Number too short (11 characters)'
|
||||
},
|
||||
server: {
|
||||
general: 'General',
|
||||
error: {
|
||||
invalid_json: 'Invalid JSON data',
|
||||
no_auth_token: 'Unauthorized, missing or invalid auth token',
|
||||
jwt_parsing_error: 'Unauthorized, auth token could not be read',
|
||||
unauthorized: 'You are not authorized to perform this action',
|
||||
internal_server_error:
|
||||
'Damn, error on our side, try again, then contact someone from the organization.',
|
||||
not_possible: 'Operation not possible.',
|
||||
not_found: 'Could not be found.',
|
||||
in_use: 'Is in use',
|
||||
undelivered_verification_mail:
|
||||
'Registration successful, but the verification email could not be sent. Please contact the organization to verify your email address and activate your account.'
|
||||
},
|
||||
validation: {
|
||||
invalid: 'Invalid',
|
||||
invalid_user_id: 'Invalid user ID',
|
||||
invalid_subscription: 'Model not found',
|
||||
user_not_found: '{field} could not be found',
|
||||
invalid_user_data: 'Invalid user data',
|
||||
user_not_found_or_wrong_password: 'Does not exist or wrong password',
|
||||
email_already_registered: 'A member has already been created with this email address.',
|
||||
password_already_changed: 'The password has already been changed.',
|
||||
alphanumunicode: 'Contains disallowed characters',
|
||||
safe_content: 'I see what you did there! Do not cross this line!',
|
||||
iban: 'Invalid. Format: DE07123412341234123412',
|
||||
bic: 'Invalid. Format: BELADEBEXXX',
|
||||
email: 'Invalid format',
|
||||
number: 'Is not a number',
|
||||
euDriversLicence: 'Is not a European driver’s licence',
|
||||
lte: 'Is too large/new',
|
||||
gt: 'Is too small/old',
|
||||
required: 'Field is required',
|
||||
image: 'This is not an image',
|
||||
alphanum: 'Contains invalid characters',
|
||||
user_disabled: 'User is disabled',
|
||||
duplicate: 'Already exists...',
|
||||
alphaunicode: 'Must consist only of letters',
|
||||
too_soon: 'Too soon'
|
||||
}
|
||||
},
|
||||
licenceCategory: {
|
||||
AM: 'Mopeds and light four-wheeled vehicles (50cc, max 45 km/h)',
|
||||
A1: 'Light motorcycles (125cc)',
|
||||
A2: 'Medium-power motorcycles (max 35 kW)',
|
||||
A: 'Motorcycles',
|
||||
B: 'Motor vehicles ≤ 3500 kg, ≤ 8 seats',
|
||||
C1: 'Medium-heavy vehicles - 7500 kg',
|
||||
C: 'Heavy commercial vehicles > 3500 kg',
|
||||
D1: 'Minibuses with 9-16 seats',
|
||||
D: 'Buses > 8 seats',
|
||||
BE: 'Vehicle class B with trailer',
|
||||
C1E: 'Vehicle class C1 with trailer',
|
||||
CE: 'Vehicle class C with trailer',
|
||||
D1E: 'Vehicle class D1 with trailer',
|
||||
DE: 'Vehicle class D with trailer',
|
||||
L: 'Agricultural, forestry vehicles, forklifts max 40 km/h',
|
||||
T: 'Agricultural, forestry vehicles, forklifts max 60 km/h'
|
||||
},
|
||||
users: 'Members',
|
||||
user: {
|
||||
login: 'User Login',
|
||||
edit: 'Edit User',
|
||||
create: 'Create User',
|
||||
user: 'User',
|
||||
management: 'Member Management',
|
||||
id: 'Member ID',
|
||||
first_name: 'First Name',
|
||||
last_name: 'Last Name',
|
||||
phone: 'Phone Number',
|
||||
dateofbirth: 'Date of Birth',
|
||||
email: 'Email',
|
||||
status: 'Status',
|
||||
role: 'User Role',
|
||||
supporter: 'Sponsor'
|
||||
},
|
||||
subscriptions: {
|
||||
name: 'Model Name',
|
||||
edit: 'Edit Model',
|
||||
create: 'Create Model',
|
||||
subscription: 'Subscription Model',
|
||||
subscriptions: 'Subscription Models',
|
||||
conditions: 'Conditions',
|
||||
monthly_fee: 'Monthly Fee',
|
||||
hourly_rate: 'Hourly Rate',
|
||||
included_hours_per_year: 'Included Hours Per Year',
|
||||
included_hours_per_month: 'Included Hours Per Month'
|
||||
},
|
||||
loading: {
|
||||
user_data: 'Loading user data',
|
||||
subscription_data: 'Loading model data',
|
||||
please_wait: 'Please wait...',
|
||||
updating: 'Updating...'
|
||||
},
|
||||
dialog: {
|
||||
user_deletion: 'Should the user {firstname} {lastname} really be deleted?',
|
||||
subscription_deletion: 'Should the subscription model {name} really be deleted?'
|
||||
},
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
actions: 'Actions',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
search: 'Search:',
|
||||
name: 'Name',
|
||||
supporter: 'Sponsors',
|
||||
mandate_date_signed: 'Mandate Signing Date',
|
||||
licence_categories: 'Driver’s licence Categories',
|
||||
subscription: 'Membership Model',
|
||||
licence: 'Driver’s licence',
|
||||
licence_number: 'Driver’s licence Number',
|
||||
issued_date: 'Issue Date',
|
||||
expiration_date: 'Expiration Date',
|
||||
country: 'Country',
|
||||
details: 'Details',
|
||||
unknown: 'Unknown',
|
||||
notes: 'Notes',
|
||||
address: 'Street & House Number',
|
||||
city: 'City',
|
||||
zip_code: 'ZIP Code',
|
||||
forgot_password: 'Forgot Password?',
|
||||
password: 'Password',
|
||||
confirm_password: 'Repeat Password',
|
||||
password_changed: 'Password successfully changed.',
|
||||
change_password: 'Change Password',
|
||||
password_change_requested: 'Password change request sent... Please check your inbox.',
|
||||
company: 'Company',
|
||||
login: 'Login',
|
||||
profile: 'Profile',
|
||||
membership: 'Membership',
|
||||
bank_account: 'Bank Account',
|
||||
status: 'Status',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
parent_member_id: 'Parent Member ID',
|
||||
bank_account_holder: 'Account Holder',
|
||||
bank_name: 'Bank Name',
|
||||
iban: 'IBAN',
|
||||
bic: 'BIC',
|
||||
mandate_reference: 'SEPA Mandate',
|
||||
payments: 'Payments',
|
||||
add_new: 'New',
|
||||
email_sent: 'Email has been sent...',
|
||||
payment: {
|
||||
id: 'Payment ID',
|
||||
amount: 'Amount',
|
||||
date: 'Date',
|
||||
status: 'Status'
|
||||
},
|
||||
subscriptionStatus: {
|
||||
pending: 'Pending',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export const BASE_API_URI = import.meta.env.DEV
|
||||
? import.meta.env.VITE_BASE_API_URI_DEV
|
||||
: import.meta.env.VITE_BASE_API_URI_PROD;
|
||||
? import.meta.env.VITE_BASE_API_URI_DEV
|
||||
: import.meta.env.VITE_BASE_API_URI_PROD;
|
||||
|
||||
export const PERMISSIONS = {
|
||||
Member: 1,
|
||||
View: 2,
|
||||
Update: 4,
|
||||
Create: 4,
|
||||
Delete: 4,
|
||||
Super: 8
|
||||
};
|
||||
|
||||
171
frontend/src/lib/utils/defaults.js
Normal file
171
frontend/src/lib/utils/defaults.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// src/lib/utils/defaults.js
|
||||
|
||||
/**
|
||||
* @returns {App.Types['subscription']}
|
||||
*/
|
||||
export function defaultSubscription() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
details: '',
|
||||
conditions: '',
|
||||
monthly_fee: 0,
|
||||
hourly_rate: 0,
|
||||
included_hours_per_year: 0,
|
||||
included_hours_per_month: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['membership']}
|
||||
*/
|
||||
export function defaultMembership() {
|
||||
return {
|
||||
id: 0,
|
||||
status: 3,
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
parent_member_id: 0,
|
||||
subscription: defaultSubscription()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['bankAccount']}
|
||||
*/
|
||||
export function defaultBankAccount() {
|
||||
return {
|
||||
id: 0,
|
||||
mandate_date_signed: '',
|
||||
bank: '',
|
||||
account_holder_name: '',
|
||||
iban: '',
|
||||
bic: '',
|
||||
mandate_reference: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['licence']}
|
||||
*/
|
||||
export function defaultLicence() {
|
||||
return {
|
||||
id: 0,
|
||||
status: 0,
|
||||
number: '',
|
||||
issued_date: '',
|
||||
expiration_date: '',
|
||||
country: '',
|
||||
categories: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Locals['user']}
|
||||
*/
|
||||
export function defaultUser() {
|
||||
return {
|
||||
id: 0,
|
||||
email: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
zip_code: '',
|
||||
city: '',
|
||||
company: '',
|
||||
dateofbirth: '',
|
||||
notes: '',
|
||||
status: 1,
|
||||
role_id: 1,
|
||||
membership: defaultMembership(),
|
||||
licence: defaultLicence(),
|
||||
bank_account: defaultBankAccount()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Locals['user']}
|
||||
*/
|
||||
export function defaultSupporter() {
|
||||
let supporter = defaultUser();
|
||||
supporter.status = 5;
|
||||
supporter.role_id = 0;
|
||||
supporter.licence = null;
|
||||
supporter.membership = null;
|
||||
return supporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Locals['user']}
|
||||
*/
|
||||
export function defaultOpponent() {
|
||||
let opponent = defaultUser();
|
||||
opponent.status = 5;
|
||||
opponent.role_id = -1;
|
||||
opponent.licence = null;
|
||||
opponent.membership = null;
|
||||
return opponent;
|
||||
}
|
||||
/**
|
||||
* @returns {App.Types['location']}
|
||||
*/
|
||||
export function defaultLocation() {
|
||||
return {
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['damage']}
|
||||
*/
|
||||
export function defaultDamage() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
opponent: defaultOpponent(),
|
||||
driver_id: -1,
|
||||
insurance: defaultInsurance(),
|
||||
date: '',
|
||||
notes: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['insurance']}
|
||||
*/
|
||||
export function defaultInsurance() {
|
||||
return {
|
||||
id: 0,
|
||||
company: '',
|
||||
reference: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
notes: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {App.Types['car']}
|
||||
*/
|
||||
export function defaultCar() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
status: 0,
|
||||
brand: '',
|
||||
model: '',
|
||||
price: 0,
|
||||
rate: 0,
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
color: '',
|
||||
licence_plate: '',
|
||||
location: defaultLocation(),
|
||||
damages: [],
|
||||
insurances: [],
|
||||
notes: ''
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { crossfade } from "svelte/transition";
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
|
||||
export const [send, receive] = crossfade({
|
||||
duration: (d) => Math.sqrt(d * 200),
|
||||
duration: (d) => Math.sqrt(d * 200),
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fallback(node, params) {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fallback(node, params) {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
return {
|
||||
duration: 600,
|
||||
easing: quintOut,
|
||||
css: (t) => `
|
||||
return {
|
||||
duration: 600,
|
||||
easing: quintOut,
|
||||
css: (t) => `
|
||||
transform: ${transform} scale(${t});
|
||||
opacity: ${t}
|
||||
`,
|
||||
};
|
||||
},
|
||||
`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -27,9 +26,9 @@ export const [send, receive] = crossfade({
|
||||
* @param {string} email - The email to validate
|
||||
*/
|
||||
export const isValidEmail = (email) => {
|
||||
const EMAIL_REGEX =
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
return EMAIL_REGEX.test(email.trim());
|
||||
const EMAIL_REGEX =
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
return EMAIL_REGEX.test(email.trim());
|
||||
};
|
||||
/**
|
||||
* Validates a strong password field
|
||||
@@ -37,11 +36,9 @@ export const isValidEmail = (email) => {
|
||||
* @param {string} password - The password to validate
|
||||
*/
|
||||
export const isValidPasswordStrong = (password) => {
|
||||
const strongRegex = new RegExp(
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
|
||||
);
|
||||
const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');
|
||||
|
||||
return strongRegex.test(password.trim());
|
||||
return strongRegex.test(password.trim());
|
||||
};
|
||||
/**
|
||||
* Validates a medium password field
|
||||
@@ -49,11 +46,11 @@ export const isValidPasswordStrong = (password) => {
|
||||
* @param {string} password - The password to validate
|
||||
*/
|
||||
export const isValidPasswordMedium = (password) => {
|
||||
const mediumRegex = new RegExp(
|
||||
"^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})"
|
||||
);
|
||||
const mediumRegex = new RegExp(
|
||||
'^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'
|
||||
);
|
||||
|
||||
return mediumRegex.test(password.trim());
|
||||
return mediumRegex.test(password.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -63,143 +60,178 @@ export const isValidPasswordMedium = (password) => {
|
||||
*/
|
||||
|
||||
export function isEmpty(obj) {
|
||||
for (const _i in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function toRFC3339(dateString) {
|
||||
if (!dateString) dateString = "0001-01-01T00:00:00.000Z";
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function fromRFC3339(dateString) {
|
||||
if (!dateString) dateString = "0001-01-01T00:00:00.000Z";
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split("T")[0];
|
||||
for (const _i in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {App.Locals.User} user - The user object to format
|
||||
* @param {string} dateString
|
||||
* @returns string
|
||||
*/
|
||||
export function toRFC3339(dateString) {
|
||||
if (!dateString || dateString == '') dateString = '0001-01-01T00:00:00.000Z';
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} dateString
|
||||
* @returns string
|
||||
*/
|
||||
export function fromRFC3339(dateString) {
|
||||
if (!dateString) dateString = '0001-01-01T00:00:00.000Z';
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {App.Types['car']} car - The car object to format
|
||||
*/
|
||||
export function carDatesFromRFC3339(car) {
|
||||
car.end_date = fromRFC3339(car.end_date);
|
||||
car.start_date = fromRFC3339(car.start_date);
|
||||
car.insurances?.forEach((insurance) => {
|
||||
insurance.start_date = fromRFC3339(insurance.start_date);
|
||||
insurance.end_date = fromRFC3339(insurance.end_date);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {App.Types['car']} car - The car object to format
|
||||
*/
|
||||
export function carDatesToRFC3339(car) {
|
||||
car.end_date = toRFC3339(car.end_date);
|
||||
car.start_date = toRFC3339(car.start_date);
|
||||
car.insurances?.forEach((insurance) => {
|
||||
insurance.start_date = toRFC3339(insurance.start_date);
|
||||
insurance.end_date = toRFC3339(insurance.end_date);
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {App.Locals['user']} user - The user object to format
|
||||
*/
|
||||
export function userDatesFromRFC3339(user) {
|
||||
if (user.date_of_birth) {
|
||||
user.date_of_birth = fromRFC3339(user.date_of_birth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = fromRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = fromRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = fromRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = fromRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = fromRFC3339(
|
||||
user.bank_account.mandate_date_signed
|
||||
);
|
||||
}
|
||||
if (user.dateofbirth) {
|
||||
user.dateofbirth = fromRFC3339(user.dateofbirth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = fromRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = fromRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = fromRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = fromRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = fromRFC3339(user.bank_account.mandate_date_signed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats dates in the user object to RFC3339 format
|
||||
* @param {App.Locals.User} user - The user object to format
|
||||
* @param {App.Locals['user']} user - The user object to format
|
||||
*/
|
||||
export function userDatesToRFC3339(user) {
|
||||
if (user.date_of_birth) {
|
||||
user.date_of_birth = toRFC3339(user.date_of_birth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = toRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = toRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = toRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = toRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = toRFC3339(
|
||||
user.bank_account.mandate_date_signed
|
||||
);
|
||||
}
|
||||
if (user.dateofbirth) {
|
||||
user.dateofbirth = toRFC3339(user.dateofbirth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = toRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = toRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = toRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = toRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = toRFC3339(user.bank_account.mandate_date_signed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} obj - The error object to format
|
||||
* @returns {array} The formatted error object
|
||||
* @param {string | Array<{field: string, key: string}> | Record<string, {key: string}>} obj - The error object to format
|
||||
* @returns {Array<{
|
||||
* field: string,
|
||||
* key: string,
|
||||
* id: number
|
||||
* }> } Array of formatted error objects
|
||||
*/
|
||||
export function formatError(obj) {
|
||||
const errors = [];
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((error) => {
|
||||
errors.push({
|
||||
field: error.field,
|
||||
key: error.key,
|
||||
id: Math.random() * 1000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(obj).forEach((field) => {
|
||||
errors.push({
|
||||
field: field,
|
||||
key: obj[field].key,
|
||||
id: Math.random() * 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
field: "general",
|
||||
key: obj,
|
||||
id: 0,
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
const errors = [];
|
||||
if (typeof obj === 'object') {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((error) => {
|
||||
errors.push({
|
||||
field: error.field,
|
||||
key: error.key,
|
||||
id: Math.random() * 1000
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(obj).forEach((field) => {
|
||||
errors.push({
|
||||
field: field,
|
||||
key: obj[field].key,
|
||||
id: Math.random() * 1000
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
field: 'general',
|
||||
key: obj,
|
||||
id: 0
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | null} newToken - The new token for the cookie to set
|
||||
* @param {import('RequestEvent<Partial<Record<string, string>>, string | null>')} event - The event object
|
||||
* @param {import('@sveltejs/kit').Cookies } cookies - The event object
|
||||
*/
|
||||
export function refreshCookie(newToken, event) {
|
||||
if (newToken) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
if (event) {
|
||||
event.cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
} else {
|
||||
cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export function refreshCookie(newToken, cookies) {
|
||||
if (newToken) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
cookies.set('jwt', match[1], {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production', // Secure in production
|
||||
sameSite: 'lax',
|
||||
maxAge: 5 * 24 * 60 * 60 // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* checks the permission of the user
|
||||
* @param {App.Locals['user']} user - The user object
|
||||
* @param {number} required_permission - The required permission
|
||||
* @returns {boolean} - True if the user has the required permission
|
||||
*/
|
||||
export function hasPrivilige(user, required_permission) {
|
||||
return user.role_id >= required_permission;
|
||||
}
|
||||
|
||||
264
frontend/src/lib/utils/processing.js
Normal file
264
frontend/src/lib/utils/processing.js
Normal file
@@ -0,0 +1,264 @@
|
||||
import { defaultBankAccount, defaultMembership } from './defaults';
|
||||
import { toRFC3339 } from './helpers';
|
||||
|
||||
/**
|
||||
* Converts FormData to a nested object structure
|
||||
* @param {FormData} formData - The FormData object to convert
|
||||
* @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']> | Partial<App.Types['car']>, confirm_password: string }} Nested object representation of the form data
|
||||
*/
|
||||
export function formDataToObject(formData) {
|
||||
/** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> | Partial<App.Types['car']> } */
|
||||
const object = {};
|
||||
let confirm_password = '';
|
||||
|
||||
console.log('Form data entries:');
|
||||
for (const [key, value] of formData.entries()) {
|
||||
console.log('Key:', key, 'Value:', value);
|
||||
if (key == 'confirm_password') {
|
||||
confirm_password = String(value);
|
||||
continue;
|
||||
}
|
||||
/** @type {string[]} */
|
||||
const keys = key.match(/\[([^\]]+)\]/g)?.map((k) => k.slice(1, -1)) || [key];
|
||||
/** @type {Record<string, any>} */
|
||||
let current = object;
|
||||
|
||||
// console.log('Current object state:', JSON.stringify(current));
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const currentKey = keys[i];
|
||||
const nextKey = keys[i + 1];
|
||||
const isNextKeyArrayIndex = !isNaN(Number(nextKey));
|
||||
if (!current[currentKey]) {
|
||||
// If next key is a number, initialize an array, otherwise an object
|
||||
current[currentKey] = isNextKeyArrayIndex ? [] : {};
|
||||
}
|
||||
/**
|
||||
* Move to the next level of the object
|
||||
* @type {Record<string, any>}
|
||||
*/
|
||||
current = current[currentKey];
|
||||
}
|
||||
|
||||
const lastKey = keys[keys.length - 1];
|
||||
if (key.endsWith('[]')) {
|
||||
current[lastKey] = current[lastKey] || [];
|
||||
try {
|
||||
/** @type {{id: number, category: string}} */
|
||||
current[lastKey].push(JSON.parse(value.toString()));
|
||||
} catch {
|
||||
current[lastKey].push(value);
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(current)) {
|
||||
// If current is an array, lastKey should be the index
|
||||
const index = parseInt(lastKey);
|
||||
current[index] = current[index] || {};
|
||||
if (keys.length > 2) {
|
||||
// For nested properties within array elements
|
||||
const propertyKey = keys[keys.length - 1];
|
||||
current[index][propertyKey] = value;
|
||||
} else {
|
||||
current[index] = value;
|
||||
}
|
||||
} else {
|
||||
current[lastKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { object: object, confirm_password: confirm_password };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected membership data structure
|
||||
* @param { App.Types['membership'] } membership - The raw form data object
|
||||
* @returns {App.Types['membership']} Processed membership data
|
||||
*/
|
||||
export function processMembershipFormData(membership) {
|
||||
return {
|
||||
id: Number(membership.id) || 0,
|
||||
status: Number(membership.status),
|
||||
start_date: toRFC3339(String(membership.start_date || '')),
|
||||
end_date: toRFC3339(String(membership.end_date || '')),
|
||||
parent_member_id: Number(membership.parent_member_id) || 0,
|
||||
subscription: processSubscriptionFormData(membership.subscription)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected licence data structure
|
||||
* @param { App.Types['licence'] } licence - The raw form data object
|
||||
* @returns {App.Types['licence']} Processed licence data
|
||||
*/
|
||||
export function processLicenceFormData(licence) {
|
||||
return {
|
||||
id: Number(licence?.id) || 0,
|
||||
status: Number(licence?.status),
|
||||
number: String(licence?.number || ''),
|
||||
issued_date: toRFC3339(String(licence?.issued_date || '')),
|
||||
expiration_date: toRFC3339(String(licence?.expiration_date || '')),
|
||||
country: String(licence?.country || ''),
|
||||
categories: licence?.categories || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected bank_account data structure
|
||||
* @param { App.Types['bankAccount'] } bank_account - The raw form data object
|
||||
* @returns {App.Types['bankAccount']} Processed bank_account data
|
||||
*/
|
||||
export function processBankAccountFormData(bank_account) {
|
||||
{
|
||||
return {
|
||||
id: Number(bank_account?.id) || 0,
|
||||
account_holder_name: String(bank_account?.account_holder_name || ''),
|
||||
bank: String(bank_account?.bank || ''),
|
||||
iban: String(bank_account?.iban || ''),
|
||||
bic: String(bank_account?.bic || ''),
|
||||
mandate_reference: String(bank_account?.mandate_reference || ''),
|
||||
mandate_date_signed: toRFC3339(String(bank_account?.mandate_date_signed || ''))
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Processes the raw form data into the expected user data structure
|
||||
* @param { Partial<App.Locals['user']> } user - The raw form data object
|
||||
* @returns {App.Locals['user']} Processed user data
|
||||
*/
|
||||
export function processUserFormData(user) {
|
||||
/** @type {App.Locals['user']} */
|
||||
let processedData = {
|
||||
id: Number(user.id) || 0,
|
||||
status: Number(user.status),
|
||||
role_id: Number(user.role_id),
|
||||
first_name: String(user.first_name),
|
||||
last_name: String(user.last_name),
|
||||
password: String(user.password) || '',
|
||||
email: String(user.email),
|
||||
phone: String(user.phone || ''),
|
||||
company: String(user.company || ''),
|
||||
dateofbirth: toRFC3339(String(user.dateofbirth || '')),
|
||||
address: String(user.address || ''),
|
||||
zip_code: String(user.zip_code || ''),
|
||||
city: String(user.city || ''),
|
||||
notes: String(user.notes || ''),
|
||||
membership: processMembershipFormData(user.membership ? user.membership : defaultMembership()),
|
||||
licence: user.licence ? processLicenceFormData(user.licence) : null,
|
||||
bank_account: processBankAccountFormData(
|
||||
user.bank_account ? user.bank_account : defaultBankAccount()
|
||||
)
|
||||
};
|
||||
// console.log('Categories: --------');
|
||||
// console.dir(rawData.object.licence);
|
||||
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
|
||||
value !== null && value !== '' ? value : undefined
|
||||
);
|
||||
console.dir(clean);
|
||||
return clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected subscription data structure
|
||||
* @param {Partial<App.Types['subscription']>} subscription - The raw form data object
|
||||
* @returns {App.Types['subscription']} Processed user data
|
||||
*/
|
||||
export function processSubscriptionFormData(subscription) {
|
||||
/** @type {Partial<App.Types['subscription']>} */
|
||||
let processedData = {
|
||||
id: Number(subscription.id) || 0,
|
||||
name: String(subscription.name) || '',
|
||||
details: String(subscription.details) || '',
|
||||
conditions: String(subscription.conditions) || '',
|
||||
hourly_rate: Number(subscription.hourly_rate) || 0,
|
||||
monthly_fee: Number(subscription.monthly_fee) || 0,
|
||||
included_hours_per_month: Number(subscription.included_hours_per_month) || 0,
|
||||
included_hours_per_year: Number(subscription.included_hours_per_year) || 0
|
||||
};
|
||||
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
|
||||
value !== null && value !== '' ? value : undefined
|
||||
);
|
||||
console.dir(clean);
|
||||
return clean;
|
||||
}
|
||||
/**
|
||||
* Processes the raw form data into the expected insurance data structure
|
||||
* @param {App.Types['insurance']} insurance - The raw form data object
|
||||
* @returns {App.Types['insurance']} Processed user data
|
||||
*/
|
||||
export function processInsuranceFormData(insurance) {
|
||||
return {
|
||||
id: Number(insurance.id) || 0,
|
||||
company: String(insurance.company) || '',
|
||||
reference: String(insurance.reference) || '',
|
||||
start_date: toRFC3339(String(insurance.start_date) || '') || '',
|
||||
end_date: toRFC3339(String(insurance.end_date) || '') || '',
|
||||
notes: String(insurance.notes) || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected car data structure
|
||||
* @param {Partial<App.Types['car']>} car - The raw form data object
|
||||
* @returns {App.Types['car']} Processed user data
|
||||
*/
|
||||
export function processCarFormData(car) {
|
||||
console.dir(car);
|
||||
/** @type {App.Types['car']} */
|
||||
let processedData = {
|
||||
id: Number(car.id) || 0,
|
||||
name: String(car.name) || '',
|
||||
status: Number(car.status) || 0,
|
||||
brand: String(car.brand) || '',
|
||||
model: String(car.model) || '',
|
||||
price: Number(car.price) || 0,
|
||||
rate: Number(car.rate) || 0,
|
||||
licence_plate: String(car.licence_plate),
|
||||
start_date: 'start_date' in car ? toRFC3339(String(car.start_date) || '') : '',
|
||||
end_date: 'end_date' in car ? toRFC3339(String(car.end_date) || '') : '',
|
||||
color: String(car.color) || '',
|
||||
notes: String(car.notes) || '',
|
||||
location:
|
||||
'location' in car
|
||||
? {
|
||||
latitude: Number(car.location?.latitude) || 0,
|
||||
longitude: Number(car.location?.longitude) || 0
|
||||
}
|
||||
: {
|
||||
latitude: 0,
|
||||
longitude: 0
|
||||
},
|
||||
damages: /** @type {App.Types['damage'][]} */ ([]),
|
||||
insurances: /** @type {App.Types['insurance'][]} */ ([])
|
||||
};
|
||||
car.insurances?.forEach((insurance) => {
|
||||
processedData.insurances.push(processInsuranceFormData(insurance));
|
||||
});
|
||||
|
||||
car.damages?.forEach((damage) => {
|
||||
console.dir(damage);
|
||||
processedData.damages.push(processDamageFormData(damage));
|
||||
});
|
||||
|
||||
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
|
||||
value !== null && value !== '' ? value : undefined
|
||||
);
|
||||
console.dir(clean);
|
||||
return clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the raw form data into the expected damage data structure
|
||||
* @param { App.Types['damage'] } damage - The raw form data object
|
||||
* @returns {App.Types['damage']} Processed damage data
|
||||
*/
|
||||
export function processDamageFormData(damage) {
|
||||
return {
|
||||
id: Number(damage.id) || 0,
|
||||
name: String(damage.name) || '',
|
||||
opponent: processUserFormData(damage.opponent),
|
||||
driver_id: Number(damage.driver_id) || 0,
|
||||
insurance: processInsuranceFormData(damage.insurance),
|
||||
date: toRFC3339(String(damage.date) || ''),
|
||||
notes: String(damage.notes) || ''
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { refreshCookie } from "$lib/utils/helpers";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ locals, cookies }) {
|
||||
return {
|
||||
user: locals.user,
|
||||
licence_categories: locals.licence_categories,
|
||||
subscriptions: locals.subscriptions,
|
||||
};
|
||||
export async function load({ locals }) {
|
||||
return {
|
||||
user: locals.user,
|
||||
licence_categories: locals.licence_categories,
|
||||
subscriptions: locals.subscriptions
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import "$lib/utils/i18n.js";
|
||||
|
||||
// import "$lib/css/bootstrap-custom.scss";
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
</script> -->
|
||||
|
||||
<div class="hero-container">
|
||||
<!-- <div class="hero-logo"><img src={Developer} alt="Alexander Stölting" /></div> -->
|
||||
<h3 class="hero-subtitle subtitle">Backend vom Carsharing Zeug</h3>
|
||||
<div class="hero-buttons-container">
|
||||
<a class="button-dark" href="https://tiny-bits.net/" data-learn-more
|
||||
>Auf zu Tiny Bits</a
|
||||
>
|
||||
</div>
|
||||
<!-- <div class="hero-logo"><img src={Developer} alt="Alexander Stölting" /></div> -->
|
||||
<h3 class="hero-subtitle subtitle">Backend vom Carsharing Zeug</h3>
|
||||
<div class="hero-buttons-container">
|
||||
<a class="button-dark" href="https://carsharing-hasloh.de/" data-learn-more
|
||||
>Auf zur Carsharing Webseite</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,187 +1,146 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { toRFC3339 } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { formatError, userDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { formDataToObject, processUserFormData } from '$lib/utils/processing';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
/**
|
||||
* @typedef {Object} UpdateData
|
||||
* @property {Partial<App.Locals['user']>} user
|
||||
*/
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals, params }) {
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `/auth/login?next=/auth/about/${params.id}`);
|
||||
}
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `${base}/auth/login?next=${base}/auth/about/${params.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateUser: async ({ request, fetch, cookies, locals }) => {
|
||||
let formData = await request.formData();
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateUser: async ({ request, fetch, cookies, locals }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const licenceCategories = formData
|
||||
.getAll("licence_categories[]")
|
||||
.filter((value) => typeof value === "string")
|
||||
.map((value) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse licence category:", value);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
const rawFormData = formDataToObject(formData);
|
||||
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
|
||||
const rawData = {
|
||||
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
|
||||
confirm_password: rawFormData.confirm_password
|
||||
};
|
||||
// confirm password matches and is not empty. Otherwise set password to empty string
|
||||
if (
|
||||
rawData.object.password &&
|
||||
rawData.confirm_password &&
|
||||
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
|
||||
) {
|
||||
rawData.object.password = '';
|
||||
}
|
||||
const processedData = processUserFormData(rawData.object);
|
||||
|
||||
/** @type {Partial<App.Locals['user']>} */
|
||||
const updateData = {
|
||||
id: Number(formData.get("id")),
|
||||
first_name: String(formData.get("first_name")),
|
||||
last_name: String(formData.get("last_name")),
|
||||
email: String(formData.get("email")),
|
||||
phone: String(formData.get("phone")),
|
||||
notes: String(formData.get("notes")),
|
||||
address: String(formData.get("address")),
|
||||
zip_code: String(formData.get("zip_code")),
|
||||
city: String(formData.get("city")),
|
||||
date_of_birth: toRFC3339(formData.get("birth_date")),
|
||||
company: String(formData.get("company")),
|
||||
profile_picture: String(formData.get("profile_picture")),
|
||||
membership: {
|
||||
id: Number(formData.get("membership_id")),
|
||||
start_date: toRFC3339(formData.get("membership_start_date")),
|
||||
end_date: toRFC3339(formData.get("membership_end_date")),
|
||||
status: Number(formData.get("membership_status")),
|
||||
parent_member_id: Number(formData.get("parent_member_id")),
|
||||
subscription_model: {
|
||||
id: Number(formData.get("subscription_model_id")),
|
||||
name: String(formData.get("subscription_model_name")),
|
||||
},
|
||||
},
|
||||
bank_account: {
|
||||
id: Number(formData.get("bank_account_id")),
|
||||
mandate_date_signed: toRFC3339(
|
||||
String(formData.get("mandate_date_signed"))
|
||||
),
|
||||
bank: String(formData.get("bank")),
|
||||
account_holder_name: String(formData.get("account_holder_name")),
|
||||
iban: String(formData.get("iban")),
|
||||
bic: String(formData.get("bic")),
|
||||
mandate_reference: String(formData.get("mandate_reference")),
|
||||
},
|
||||
licence: {
|
||||
id: Number(formData.get("drivers_licence_id")),
|
||||
status: Number(formData.get("licence_status")),
|
||||
licence_number: String(formData.get("licence_number")),
|
||||
issued_date: toRFC3339(formData.get("issued_date")),
|
||||
expiration_date: toRFC3339(formData.get("expiration_date")),
|
||||
country: String(formData.get("country")),
|
||||
licence_categories: licenceCategories,
|
||||
},
|
||||
};
|
||||
// Remove undefined or null properties
|
||||
const cleanUpdateData = JSON.parse(
|
||||
JSON.stringify(updateData),
|
||||
(key, value) => (value !== null && value !== "" ? value : undefined)
|
||||
);
|
||||
console.dir(formData);
|
||||
console.dir(cleanUpdateData);
|
||||
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
||||
// const isCreating = !processedData.user.id || processedData.user.id === 0;
|
||||
// console.log('Is creating: ', isCreating);
|
||||
const apiURL = `${BASE_API_URI}/auth/users/`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestUpdateOptions = {
|
||||
method: "PATCH",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
body: JSON.stringify(cleanUpdateData),
|
||||
};
|
||||
const res = await fetch(apiURL, requestUpdateOptions);
|
||||
/** @type {RequestInit} */
|
||||
const requestUpdateOptions = {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify(processedData)
|
||||
};
|
||||
const res = await fetch(apiURL, requestUpdateOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
locals.user = response;
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `/auth/about/${response.id}`);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
uploadImage: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
const response = await res.json();
|
||||
locals.user = response;
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `${base}/auth/about/${response.id}`);
|
||||
},
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
body: formData,
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
uploadImage: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: formData
|
||||
};
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
const res = await fetch(`${BASE_API_URI}/file/upload/`, requestInitOptions);
|
||||
|
||||
const response = await res.json();
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile_picture: response[""],
|
||||
};
|
||||
},
|
||||
const response = await res.json();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
deleteImage: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
return {
|
||||
success: true,
|
||||
profile_picture: response['']
|
||||
};
|
||||
},
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
body: formData,
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
deleteImage: async ({ request, fetch, cookies }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: formData
|
||||
};
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
const res = await fetch(`${BASE_API_URI}/file/delete/`, requestInitOptions);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile_picture: "",
|
||||
};
|
||||
},
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
profile_picture: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,156 +1,164 @@
|
||||
<script>
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import UserEditForm from "$lib/components/UserEditForm.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "svelte-i18n";
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import UserEditForm from '$lib/components/UserEditForm.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
$: ({ user, licence_categories, subscriptions } = $page.data);
|
||||
$: ({ user, licence_categories, subscriptions } = $page.data);
|
||||
|
||||
let showModal = false;
|
||||
let showModal = false;
|
||||
|
||||
const open = () => (showModal = true);
|
||||
const close = () => {
|
||||
showModal = false;
|
||||
if (form) {
|
||||
form.errors = undefined;
|
||||
}
|
||||
};
|
||||
const open = () => (showModal = true);
|
||||
const close = () => {
|
||||
showModal = false;
|
||||
if (form) {
|
||||
form.errors = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
console.dir(user);
|
||||
});
|
||||
onMount(() => {
|
||||
console.dir(user);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="hero-container">
|
||||
<div class="user-info">
|
||||
{#if user.status}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Status:</span>
|
||||
<span class="value block-value">
|
||||
<span
|
||||
>{$t(`userStatus.${user.status}`, {
|
||||
default: "unknown status",
|
||||
})}</span
|
||||
>
|
||||
<span
|
||||
>{$t(`userRole.${user.role_id}`, { default: "unknown role" })}</span
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Name:</span>
|
||||
<span class="value">{`${user.first_name} ${user.last_name}`}</span>
|
||||
</h3>
|
||||
{#if user.email}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Email:</span>
|
||||
<span class="value">{user.email}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.address}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Adresse:</span>
|
||||
<span class="value block-value">
|
||||
<span>{user.address}</span>
|
||||
<span>{`${user.zip_code} ${user.city}`}</span>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.phone}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Telefon:</span>
|
||||
<span class="value">{user.phone}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.date_of_birth}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Geburtstag:</span>
|
||||
<span class="value">{user.date_of_birth}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.notes}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">{$t("notes")}:</span>
|
||||
<span class="value">{user.notes}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="user-info">
|
||||
{#if user.status}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Status:</span>
|
||||
<span class="value block-value">
|
||||
<span
|
||||
>{$t(`userStatus.${user.status}`, {
|
||||
default: 'unknown status'
|
||||
})}</span
|
||||
>
|
||||
<span>{$t(`userRole.${user.role_id}`, { default: 'unknown' })}</span>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Name:</span>
|
||||
<span class="value">{`${user.first_name} ${user.last_name}`}</span>
|
||||
</h3>
|
||||
{#if user.email}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Email:</span>
|
||||
<span class="value">{user.email}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.address}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Adresse:</span>
|
||||
<span class="value block-value">
|
||||
<span>{user.address}</span>
|
||||
<span>{`${user.zip_code} ${user.city}`}</span>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.phone}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Telefon:</span>
|
||||
<span class="value">{user.phone}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.dateofbirth}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Geburtstag:</span>
|
||||
<span class="value">{user.dateofbirth}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.notes}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">{$t('notes')}:</span>
|
||||
<span class="value">{user.notes}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="hero-buttons-container">
|
||||
<button class="button-dark" on:click={open}>Ändern</button>
|
||||
</div>
|
||||
<div class="hero-buttons-container">
|
||||
<button class="button-dark" on:click={open}>Ändern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
{user}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:cancel={close}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
{user}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:close={close}
|
||||
on:cancel={close}
|
||||
editor={user}
|
||||
/>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.hero-container .hero-subtitle:not(:last-of-type) {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
.hero-container .hero-subtitle:not(:last-of-type) {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: start;
|
||||
text-align: left;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.user-info {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: start;
|
||||
text-align: left;
|
||||
margin-top: 1rem;
|
||||
color: var(--text);
|
||||
background-color: var(--surface0);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: contents;
|
||||
}
|
||||
.info-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.label {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-right: 1rem;
|
||||
color: var(--lavender);
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
text-align: left;
|
||||
}
|
||||
.value {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
text-align: left;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.block-value {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.block-value {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
color: var(--subtext0);
|
||||
}
|
||||
|
||||
.hero-buttons-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.hero-buttons-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.user-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.label,
|
||||
.value {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.label,
|
||||
.value {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @type {import('./$types').LayoutLoad} */
|
||||
export async function load({ fetch, url, data }) {
|
||||
const { users } = data;
|
||||
return {
|
||||
users: data.users,
|
||||
user: data.user,
|
||||
};
|
||||
export async function load({ data }) {
|
||||
return {
|
||||
users: data.users,
|
||||
user: data.user,
|
||||
cars: data.cars
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,56 +1,69 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { userDatesFromRFC3339, refreshCookie } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { userDatesFromRFC3339, refreshCookie, carDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, fetch, locals }) {
|
||||
// if (locals.users) {
|
||||
// return {
|
||||
// users: locals.users,
|
||||
// user: locals.user,
|
||||
// };
|
||||
// }
|
||||
const jwt = cookies.get('jwt');
|
||||
try {
|
||||
const [usersResponse, carsResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/auth/users`, {
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/auth/cars`, {
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
})
|
||||
]);
|
||||
if (!usersResponse.ok || !carsResponse.ok) {
|
||||
cookies.delete('jwt', { path: '/' });
|
||||
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`);
|
||||
}
|
||||
const [usersData, carsData] = await Promise.all([usersResponse.json(), carsResponse.json()]);
|
||||
// const response = await fetch(`${BASE_API_URI}/auth/users/`, {
|
||||
// credentials: 'include',
|
||||
// headers: {
|
||||
// Cookie: `jwt=${jwt}`
|
||||
// }
|
||||
// });
|
||||
// if (!response.ok) {
|
||||
// // Clear the invalid JWT cookie
|
||||
// cookies.delete('jwt', { path: '/' });
|
||||
// throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`);
|
||||
// }
|
||||
|
||||
const jwt = cookies.get("jwt");
|
||||
try {
|
||||
// Fetch user data, subscriptions, and licence categories in parallel
|
||||
const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
throw redirect(302, "/auth/login?next=/");
|
||||
}
|
||||
// const data = await response.json();
|
||||
/** @type {App.Locals['users']}*/
|
||||
const users = usersData.users;
|
||||
/** @type {App.Types['car'][]} */
|
||||
const cars = carsData.cars;
|
||||
|
||||
const data = await response.json();
|
||||
users.forEach((user) => {
|
||||
userDatesFromRFC3339(user);
|
||||
});
|
||||
cars.forEach((car) => {
|
||||
carDatesFromRFC3339(car);
|
||||
});
|
||||
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
refreshCookie(newToken, null);
|
||||
locals.users = users;
|
||||
locals.cars = cars;
|
||||
// Check if the server sent a new token
|
||||
const newToken = usersResponse.headers.get('Set-Cookie');
|
||||
refreshCookie(newToken, cookies);
|
||||
return {
|
||||
subscriptions: locals.subscriptions,
|
||||
licence_categories: locals.licence_categories,
|
||||
users: locals.users,
|
||||
user: locals.user,
|
||||
cars: locals.cars
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
// In case of any error, clear the JWT cookie
|
||||
cookies.delete('jwt', { path: '/' });
|
||||
|
||||
/** @type {App.Locals['users']}*/
|
||||
const users = data.users;
|
||||
|
||||
users.forEach((user) => {
|
||||
userDatesFromRFC3339(user);
|
||||
});
|
||||
|
||||
locals.users = users;
|
||||
return {
|
||||
subscriptions: locals.subscriptions,
|
||||
licence_categories: locals.licence_categories,
|
||||
users: locals.users,
|
||||
user: locals.user,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
// In case of any error, clear the JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
|
||||
throw redirect(302, "/auth/login?next=/");
|
||||
}
|
||||
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,121 +2,334 @@
|
||||
// - Implement a load function to fetch a list of all users.
|
||||
// - Create actions for updating user information (similar to the about/[id] route).
|
||||
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { toRFC3339 } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI, PERMISSIONS } from '$lib/utils/constants';
|
||||
import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import {
|
||||
formDataToObject,
|
||||
processCarFormData,
|
||||
processSubscriptionFormData,
|
||||
processUserFormData
|
||||
} from '$lib/utils/processing';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals, params }) {
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `/auth/login?next=/auth/users`);
|
||||
}
|
||||
export async function load({ locals }) {
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users`);
|
||||
}
|
||||
if (!hasPrivilige(locals.user, PERMISSIONS.View)) {
|
||||
throw redirect(302, `${base}/auth/about/${locals.user.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateUser: async ({ request, fetch, cookies, locals }) => {
|
||||
let formData = await request.formData();
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateUser: async ({ request, fetch, cookies, locals }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const licenceCategories = formData
|
||||
.getAll("licence_categories[]")
|
||||
.filter((value) => typeof value === "string")
|
||||
.map((value) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse licence category:", value);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
const rawFormData = formDataToObject(formData);
|
||||
/** @type {{object: App.Locals['user'], confirm_password: string}} */
|
||||
const rawData = {
|
||||
object: /** @type {App.Locals['user']} */ (rawFormData.object),
|
||||
confirm_password: rawFormData.confirm_password
|
||||
};
|
||||
// confirm password matches and is not empty. Otherwise set password to empty string
|
||||
if (
|
||||
rawData.object.password &&
|
||||
rawData.confirm_password &&
|
||||
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
|
||||
) {
|
||||
rawData.object.password = '';
|
||||
}
|
||||
const user = processUserFormData(rawData.object);
|
||||
|
||||
/** @type {Partial<App.Locals['user']>} */
|
||||
const updateData = {
|
||||
id: Number(formData.get("id")),
|
||||
first_name: String(formData.get("first_name")),
|
||||
last_name: String(formData.get("last_name")),
|
||||
email: String(formData.get("email")),
|
||||
phone: String(formData.get("phone")),
|
||||
notes: String(formData.get("notes")),
|
||||
address: String(formData.get("address")),
|
||||
zip_code: String(formData.get("zip_code")),
|
||||
city: String(formData.get("city")),
|
||||
date_of_birth: toRFC3339(formData.get("birth_date")),
|
||||
company: String(formData.get("company")),
|
||||
profile_picture: String(formData.get("profile_picture")),
|
||||
membership: {
|
||||
id: Number(formData.get("membership_id")),
|
||||
start_date: toRFC3339(formData.get("membership_start_date")),
|
||||
end_date: toRFC3339(formData.get("membership_end_date")),
|
||||
status: Number(formData.get("membership_status")),
|
||||
parent_member_id: Number(formData.get("parent_member_id")),
|
||||
subscription_model: {
|
||||
id: Number(formData.get("subscription_model_id")),
|
||||
name: String(formData.get("subscription_model_name")),
|
||||
},
|
||||
},
|
||||
bank_account: {
|
||||
id: Number(formData.get("bank_account_id")),
|
||||
mandate_date_signed: toRFC3339(
|
||||
String(formData.get("mandate_date_signed"))
|
||||
),
|
||||
bank: String(formData.get("bank")),
|
||||
account_holder_name: String(formData.get("account_holder_name")),
|
||||
iban: String(formData.get("iban")),
|
||||
bic: String(formData.get("bic")),
|
||||
mandate_reference: String(formData.get("mandate_reference")),
|
||||
},
|
||||
licence: {
|
||||
id: Number(formData.get("drivers_licence_id")),
|
||||
status: Number(formData.get("licence_status")),
|
||||
licence_number: String(formData.get("licence_number")),
|
||||
issued_date: toRFC3339(formData.get("issued_date")),
|
||||
expiration_date: toRFC3339(formData.get("expiration_date")),
|
||||
country: String(formData.get("country")),
|
||||
licence_categories: licenceCategories,
|
||||
},
|
||||
};
|
||||
// Remove undefined or null properties
|
||||
const cleanUpdateData = JSON.parse(
|
||||
JSON.stringify(updateData),
|
||||
(key, value) => (value !== null && value !== "" ? value : undefined)
|
||||
);
|
||||
console.dir(formData);
|
||||
console.dir(cleanUpdateData);
|
||||
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
||||
console.dir(user.membership);
|
||||
const isCreating = !user.id || user.id === 0;
|
||||
console.log('Is creating: ', isCreating);
|
||||
const apiURL = `${BASE_API_URI}/auth/users`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestUpdateOptions = {
|
||||
method: "PATCH",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
body: JSON.stringify(cleanUpdateData),
|
||||
};
|
||||
const res = await fetch(apiURL, requestUpdateOptions);
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: isCreating ? 'POST' : 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify(user)
|
||||
};
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
const response = await res.json();
|
||||
locals.user = response;
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `/auth/about/${response.id}`);
|
||||
},
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
locals.user = response;
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateSubscription: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const rawFormData = formDataToObject(formData);
|
||||
const rawSubscription = /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object);
|
||||
const subscription = processSubscriptionFormData(rawSubscription);
|
||||
|
||||
const isCreating = !subscription.id || subscription.id === 0;
|
||||
console.log('Is creating: ', isCreating);
|
||||
const apiURL = `${BASE_API_URI}/auth/subscriptions`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: isCreating ? 'POST' : 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateCar: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
console.dir(formData);
|
||||
const rawCar = /**@type {Partial<App.Types['car']>} */ (formDataToObject(formData).object);
|
||||
const car = processCarFormData(rawCar);
|
||||
|
||||
const isCreating = !car.id || car.id === 0;
|
||||
console.log('Is creating: ', isCreating);
|
||||
console.log('sending: ', JSON.stringify(car.damages));
|
||||
|
||||
const apiURL = `${BASE_API_URI}/auth/cars`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: isCreating ? 'POST' : 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify(car)
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
console.log('Server opponent response:', response.damages[0]?.opponent);
|
||||
console.log('Server insurance response:', response.damages[0]?.insurance);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @param locals - The local object, housing current user
|
||||
* @returns
|
||||
*/
|
||||
userDelete: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const rawFormData = formDataToObject(formData);
|
||||
/** @type {Partial<App.Locals['user']>} */
|
||||
const rawUser = /** @type {Partial<App.Locals['user']>} */ (rawFormData.object);
|
||||
|
||||
const apiURL = `${BASE_API_URI}/auth/users`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify({ id: Number(rawUser.id) })
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns
|
||||
*/
|
||||
subscriptionDelete: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const rawData = formDataToObject(formData);
|
||||
|
||||
/** @type {Partial<App.Types['subscription']>} */
|
||||
const subscription = rawData.object;
|
||||
|
||||
const apiURL = `${BASE_API_URI}/auth/subscriptions`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify({ id: Number(subscription.id), name: subscription.name })
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns
|
||||
*/
|
||||
carDelete: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
console.dir(formData);
|
||||
const rawCar = formDataToObject(formData);
|
||||
|
||||
const apiURL = `${BASE_API_URI}/auth/cars`;
|
||||
console.log('sending delete request to', JSON.stringify({ id: Number(rawCar.object.id) }));
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify({ id: Number(rawCar.object.id) })
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns
|
||||
*/
|
||||
grantBackendAccess: async ({ request, fetch, cookies }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const rawFormData = formDataToObject(formData);
|
||||
/** @type {App.Locals['user']} */
|
||||
const rawUser = /** @type {App.Locals['user']} */ (rawFormData.object);
|
||||
const processedData = processUserFormData(rawUser);
|
||||
console.dir(processedData);
|
||||
const apiURL = `${BASE_API_URI}/auth/users/activate`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
},
|
||||
body: JSON.stringify(processedData)
|
||||
};
|
||||
|
||||
const res = await fetch(apiURL, requestOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
console.log('Server success response:', response);
|
||||
throw redirect(303, `${base}/auth/admin/users`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,92 +1,916 @@
|
||||
<!-- - Create a table or list view of all users.
|
||||
- Implement a search or filter functionality.
|
||||
- Add a modal component for editing user details (reuse the modal from about/[id]). -->
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import UserEditForm from "$lib/components/UserEditForm.svelte";
|
||||
import { t } from "svelte-i18n";
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import UserEditForm from '$lib/components/UserEditForm.svelte';
|
||||
import SubscriptionEditForm from '$lib/components/SubscriptionEditForm.svelte';
|
||||
import InputField from '$lib/components/InputField.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
|
||||
import { PERMISSIONS } from '$lib/utils/constants';
|
||||
import {
|
||||
defaultCar,
|
||||
defaultSubscription,
|
||||
defaultSupporter,
|
||||
defaultUser
|
||||
} from '$lib/utils/defaults';
|
||||
import CarEditForm from '$lib/components/CarEditForm.svelte';
|
||||
|
||||
import { page } from "$app/stores";
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
$: ({
|
||||
user = [],
|
||||
users = [],
|
||||
cars = [],
|
||||
licence_categories = [],
|
||||
subscriptions = [],
|
||||
payments = []
|
||||
} = $page.data);
|
||||
|
||||
$: ({ user, users, licence_categories, subscriptions } = $page.data);
|
||||
let activeSection = 'members';
|
||||
|
||||
/** @type(App.Locals['user'] | null) */
|
||||
let selectedUser = null;
|
||||
let showModal = false;
|
||||
/** @type{App.Types['car'] | App.Types['subscription'] | App.Locals['user'] | null} */
|
||||
let selected = null;
|
||||
|
||||
/**
|
||||
* Opens the edit modal for the selected user.
|
||||
* @param {App.Locals['user']} user The user to edit.
|
||||
*/
|
||||
const openEditModal = (user) => {
|
||||
selectedUser = user;
|
||||
showModal = true;
|
||||
};
|
||||
let searchTerm = '';
|
||||
|
||||
/**
|
||||
* Opens the delete modal for the selected user.
|
||||
* @param {App.Locals['user']} user The user to edit.
|
||||
*/
|
||||
const openDelete = (user) => {};
|
||||
$: members = users.filter((/** @type{App.Locals['user']} */ user) => {
|
||||
return user.role_id >= PERMISSIONS.Member;
|
||||
});
|
||||
$: supporters = users.filter((/** @type{App.Locals['user']} */ user) => {
|
||||
return user.role_id < PERMISSIONS.Member;
|
||||
});
|
||||
$: filteredMembers = searchTerm ? getFilteredUsers(members) : members;
|
||||
|
||||
const close = () => {
|
||||
showModal = false;
|
||||
selectedUser = null;
|
||||
if (form) {
|
||||
form.errors = undefined;
|
||||
}
|
||||
};
|
||||
$: filteredSupporters = searchTerm ? getFilteredUsers(supporters) : supporters;
|
||||
|
||||
/**
|
||||
* Handles Mail button click to open a formatted mailto link
|
||||
* @param {App.Locals['user'][]} filteredUsers - the users to send the mail to
|
||||
*/
|
||||
function handleMailButtonClick(filteredUsers) {
|
||||
const subject = 'Important Announcement';
|
||||
const body = `Hello everyone,\n\nThis is an important message.`;
|
||||
const bccEmails = filteredUsers
|
||||
.map((/** @type{App.Locals['user']}*/ user) => user.email)
|
||||
.join(',');
|
||||
const encodedSubject = encodeURIComponent(subject);
|
||||
const encodedBody = encodeURIComponent(body);
|
||||
const mailtoLink = `mailto:?bcc=${bccEmails}&subject=${encodedSubject}&body=${encodedBody}`;
|
||||
window.location.href = mailtoLink; // Open the mail client
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a set of members depending on the entered search query
|
||||
* @param {App.Locals['user'][]} userSet Set to filter
|
||||
* @return {App.Locals['user'][]}*/
|
||||
const getFilteredUsers = (userSet) => {
|
||||
if (!searchTerm.trim()) return userSet;
|
||||
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
|
||||
return userSet.filter((/** @type{App.Locals['user']}*/ user) => {
|
||||
const basicMatch = [
|
||||
user.first_name?.toLowerCase(),
|
||||
user.last_name?.toLowerCase(),
|
||||
user.email?.toLowerCase(),
|
||||
user.address?.toLowerCase(),
|
||||
user.city?.toLowerCase(),
|
||||
user.dateofbirth?.toLowerCase(),
|
||||
user.phone?.toLowerCase(),
|
||||
user.company?.toLowerCase(),
|
||||
user.licence?.number?.toLowerCase()
|
||||
].some((field) => field?.includes(term));
|
||||
|
||||
const subscriptionMatch = user.membership?.subscription?.name?.toLowerCase().includes(term);
|
||||
|
||||
const licenceCategoryMatch = user.licence?.categories?.some((cat) =>
|
||||
cat.category.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
const addressMatch = [
|
||||
user.address?.toLowerCase(),
|
||||
user.zip_code?.toLowerCase(),
|
||||
user.city?.toLowerCase()
|
||||
].some((field) => field?.includes(term));
|
||||
return basicMatch || subscriptionMatch || licenceCategoryMatch || addressMatch;
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
selected = null;
|
||||
if (form) {
|
||||
form.errors = [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* sets the active admin section for display
|
||||
* @param {string} section The new active section.
|
||||
*/
|
||||
const setActiveSection = (section) => {
|
||||
activeSection = section;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="admin-users-page">
|
||||
<h1>{$t("user.management")}</h1>
|
||||
<div class="container">
|
||||
<div class="layout">
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<ul class="nav-list">
|
||||
<li>
|
||||
<button
|
||||
class="nav-link {activeSection === 'members' ? 'active' : ''}"
|
||||
on:click={() => setActiveSection('members')}
|
||||
>
|
||||
<i class="fas fa-users"></i>
|
||||
{$t('users')}
|
||||
<span class="nav-badge">{members.length}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="nav-link {activeSection === 'supporter' ? 'active' : ''}"
|
||||
on:click={() => setActiveSection('supporter')}
|
||||
>
|
||||
<i class="fas fa-hand-holding-dollar"></i>
|
||||
{$t('supporter')}
|
||||
<span class="nav-badge">{supporters.length}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="nav-link {activeSection === 'subscriptions' ? 'active' : ''}"
|
||||
on:click={() => setActiveSection('subscriptions')}
|
||||
>
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
{$t('subscriptions.subscriptions')}
|
||||
<span class="nav-badge">{subscriptions.length}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="nav-link {activeSection === 'cars' ? 'active' : ''}"
|
||||
on:click={() => setActiveSection('cars')}
|
||||
>
|
||||
<i class="fas fa-car"></i>
|
||||
{$t('cars')}
|
||||
<span class="nav-badge">{cars.length}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="nav-link {activeSection === 'payments' ? 'active' : ''}"
|
||||
on:click={() => setActiveSection('payments')}
|
||||
>
|
||||
<i class="fas fa-credit-card"></i>
|
||||
{$t('payments')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="search-filter" />
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive|global={{ key: error.id }}
|
||||
out:send|global={{ key: error.id }}
|
||||
>
|
||||
{$t(error.field) + ': ' + $t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<table class="user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("user.id")}</th>
|
||||
<th>{$t("name")}</th>
|
||||
<th>{$t("email")}</th>
|
||||
<th>{$t("status")}</th>
|
||||
<th>{$t("actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user}
|
||||
<tr>
|
||||
<td>{user.id}</td>
|
||||
<td>{user.first_name} {user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{$t("userStatus." + user.status)}</td>
|
||||
<td>
|
||||
<button on:click={() => openEditModal(user)}>{$t("edit")}</button>
|
||||
<button on:click={() => openDelete(user)}>{$t("delete")}</button>
|
||||
</td>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" />
|
||||
|
||||
{#if showModal}
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
user={selectedUser}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:cancel={close}
|
||||
/>
|
||||
</Modal>
|
||||
{/if}
|
||||
{#if activeSection === 'members'}
|
||||
<div class="section-header">
|
||||
<h2>{$t('users')}</h2>
|
||||
<div class="title-container">
|
||||
<InputField
|
||||
name="search"
|
||||
bind:value={searchTerm}
|
||||
placeholder={$t('placeholder.search')}
|
||||
backgroundColor="--base"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn primary"
|
||||
aria-label="Mail Users"
|
||||
on:click={() => handleMailButtonClick(filteredMembers)}
|
||||
>
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultUser();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
{#each filteredMembers as user}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
{user.first_name}
|
||||
{user.last_name}
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{$t('user.id')}</th>
|
||||
<td>{user.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/grantBackendAccess"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
}}
|
||||
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
|
||||
if (
|
||||
!confirm(
|
||||
$t('dialog.backend_access', {
|
||||
values: {
|
||||
firstname: user.first_name || '',
|
||||
lastname: user.last_name || ''
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
e.preventDefault(); // Cancel form submission if user declines
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="user[id]" value={user.id} />
|
||||
<button class="button-dark" type="submit">
|
||||
<i class="fas fa-unlock-keyhole"></i>
|
||||
{$t('grant_backend_access')}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('name')}</th>
|
||||
<td>{user.first_name} {user.last_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('user.email')}</th>
|
||||
<td>{user.email}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.subscription')}</th>
|
||||
<td>{user.membership?.subscription?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('status')}</th>
|
||||
<td>{$t('userStatus.' + user.status)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = user;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/userDelete"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
}}
|
||||
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
|
||||
if (
|
||||
!confirm(
|
||||
$t('dialog.user_deletion', {
|
||||
values: {
|
||||
firstname: user.first_name || '',
|
||||
lastname: user.last_name || ''
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
e.preventDefault(); // Cancel form submission if user declines
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="user[id]" value={user.id} />
|
||||
<input type="hidden" name="user[last_name]" value={user.last_name} />
|
||||
<button class="btn danger" type="submit">
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if activeSection === 'supporter'}
|
||||
<div class="section-header">
|
||||
<h2>{$t('supporter')}</h2>
|
||||
<div class="title-container">
|
||||
<InputField
|
||||
name="search"
|
||||
bind:value={searchTerm}
|
||||
placeholder={$t('placeholder.search')}
|
||||
backgroundColor="--base"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn primary"
|
||||
aria-label="Mail Supporter"
|
||||
on:click={() => handleMailButtonClick(filteredSupporters)}
|
||||
>
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultSupporter();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
{#each filteredSupporters as user}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
{user.company}
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{$t('name')}</th>
|
||||
<td>{user.first_name} {user.last_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('phone')}</th>
|
||||
<td>{user.phone}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('user.email')}</th>
|
||||
<td>{user.email}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('status')}</th>
|
||||
<td>{$t('userStatus.' + user.status)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
{#if hasPrivilige(user, PERMISSIONS.Update)}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = user;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasPrivilige(user, PERMISSIONS.Delete)}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/userDelete"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
}}
|
||||
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
|
||||
if (
|
||||
!confirm(
|
||||
$t('dialog.user_deletion', {
|
||||
values: {
|
||||
firstname: user.first_name || '',
|
||||
lastname: user.last_name || ''
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
e.preventDefault(); // Cancel form submission if user declines
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="user[id]" value={user.id} />
|
||||
<input type="hidden" name="user[last_name]" value={user.last_name} />
|
||||
<button class="btn danger" type="submit">
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if activeSection === 'subscriptions'}
|
||||
<div class="section-header">
|
||||
<h2>{$t('subscriptions.subscriptions')}</h2>
|
||||
{#if hasPrivilige(user, PERMISSIONS.Super)}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultSubscription();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="accordion">
|
||||
{#each subscriptions as subscription}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
{subscription.name}
|
||||
<span class="nav-badge"
|
||||
>{members.filter(
|
||||
(/** @type{App.Locals['user']}*/ user) =>
|
||||
user.membership?.subscription?.name === subscription.name
|
||||
).length}</span
|
||||
>
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.monthly_fee')}</th>
|
||||
<td
|
||||
>{subscription.monthly_fee !== -1
|
||||
? subscription.monthly_fee + '€'
|
||||
: '-'}</td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.hourly_rate')}</th>
|
||||
<td
|
||||
>{subscription.hourly_rate !== -1
|
||||
? subscription.hourly_rate + '€'
|
||||
: '-'}</td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.included_hours_per_year')}</th>
|
||||
<td>{subscription.included_hours_per_year || 0}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.included_hours_per_month')}</th>
|
||||
<td>{subscription.included_hours_per_month || 0}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('details')}</th>
|
||||
<td>{subscription.details || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('subscriptions.conditions')}</th>
|
||||
<td>{subscription.conditions || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
{#if hasPrivilige(user, PERMISSIONS.Super)}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = subscription;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription?.id === subscription.id)}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/subscriptionDelete"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
await applyAction(result);
|
||||
} else {
|
||||
document
|
||||
.querySelector('.accordion-content')
|
||||
?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
}}
|
||||
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
|
||||
if (
|
||||
!confirm(
|
||||
$t('dialog.subscription_deletion', {
|
||||
values: {
|
||||
name: subscription.name || ''
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
e.preventDefault(); // Cancel form submission if user declines
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="subscription[id]" value={subscription.id} />
|
||||
<input type="hidden" name="subscription[name]" value={subscription.name} />
|
||||
<button class="btn danger" type="submit">
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if activeSection === 'cars'}
|
||||
<div class="section-header">
|
||||
<h2>{$t('cars')}</h2>
|
||||
{#if hasPrivilige(user, PERMISSIONS.Super)}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = defaultCar();
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
{$t('add_new')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="accordion">
|
||||
{#each cars as car}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
{car.model + ' (' + car.licence_plate + ')'}
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{$t('car.model')}</th>
|
||||
<td>{car.brand + ' ' + car.model + ' (' + car.color + ')'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('price')}</th>
|
||||
<td
|
||||
>{car.price + '€'}{car.rate
|
||||
? ' + ' + car.rate + '€/' + $t('month')
|
||||
: ''}</td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('car.damages')}</th>
|
||||
<td>{car.damages?.length || 0}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('insurance')}</th>
|
||||
<td
|
||||
>{car.insurance
|
||||
? car.insurance.company + '(' + car.insurance.reference + ')'
|
||||
: '-'}</td
|
||||
>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('car.end_date')}</th>
|
||||
<td>{car.end_date || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
{#if hasPrivilige(user, PERMISSIONS.Update)}
|
||||
<button
|
||||
class="btn primary"
|
||||
on:click={() => {
|
||||
selected = car;
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
{$t('edit')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasPrivilige(user, PERMISSIONS.Delete)}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/carDelete"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success' || result.type === 'redirect') {
|
||||
await applyAction(result);
|
||||
} else {
|
||||
document
|
||||
.querySelector('.accordion-content')
|
||||
?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
await applyAction(result);
|
||||
}
|
||||
};
|
||||
}}
|
||||
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
|
||||
if (
|
||||
!confirm(
|
||||
$t('dialog.car_deletion', {
|
||||
values: {
|
||||
name: car.brand + ' ' + car.model + ' (' + car.licence_plate + ')'
|
||||
}
|
||||
})
|
||||
)
|
||||
) {
|
||||
e.preventDefault(); // Cancel form submission if user declines
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="car[id]" value={car.id} />
|
||||
<button class="btn danger" type="submit">
|
||||
<i class="fas fa-trash"></i>
|
||||
{$t('delete')}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if activeSection === 'payments'}
|
||||
<h2>{$t('payments')}</h2>
|
||||
<div class="accordion">
|
||||
{#each payments as payment}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
Payment #{payment.id} - {payment.amount}€
|
||||
</summary>
|
||||
<div class="accordion-content">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td>{new Date(payment.date).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>{payment.status}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selected && 'email' in selected}
|
||||
// user
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
editor={user}
|
||||
user={selected}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:cancel={close}
|
||||
on:close={close}
|
||||
/>
|
||||
</Modal>
|
||||
{:else if selected && 'monthly_fee' in selected}
|
||||
//subscription
|
||||
<Modal on:close={close}>
|
||||
<SubscriptionEditForm
|
||||
{form}
|
||||
{user}
|
||||
subscription={selected}
|
||||
on:cancel={close}
|
||||
on:close={close}
|
||||
/>
|
||||
</Modal>
|
||||
{:else if selected && 'brand' in selected}
|
||||
<Modal on:close={close}>
|
||||
<CarEditForm {form} editor={user} {users} car={selected} on:cancel={close} on:close={close} />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.title-container {
|
||||
margin: 0 1rem;
|
||||
flex-grow: 1;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap; /* Allows wrapping on small screens */
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
gap: 2rem;
|
||||
height: 100%;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background: var(--surface0);
|
||||
border-right: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--subtext0);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
transition: color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--surface1);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--surface2);
|
||||
color: var(--lavender);
|
||||
border-left: 3px solid var(--mauve);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
min-width: 75%;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
border: none;
|
||||
background: var(--surface0);
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
color: var(--text);
|
||||
background: var(--surface1);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
padding: 1rem;
|
||||
background: var(--surface0);
|
||||
border-top: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #2f2f2f;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th {
|
||||
color: var(--subtext1);
|
||||
}
|
||||
|
||||
.table td {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: static;
|
||||
width: 100%;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-top: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
color: var(--lavender);
|
||||
}
|
||||
/* Additional styles for better visual hierarchy */
|
||||
details[open] .accordion-header {
|
||||
background: var(--surface2);
|
||||
color: var(--lavender);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-link:focus,
|
||||
.accordion-header:focus {
|
||||
outline: 2px solid var(--mauve);
|
||||
outline-offset: -2px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Add subtle transitions */
|
||||
.accordion-item,
|
||||
.accordion-header,
|
||||
.nav-link {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
17
frontend/src/routes/auth/confirming/+page.svelte
Normal file
17
frontend/src/routes/auth/confirming/+page.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { page } from '$app/state';
|
||||
import { t } from 'svelte-i18n';
|
||||
let message = '';
|
||||
if (page.url.search) {
|
||||
message = page.url.search.split('=')[1].replaceAll('%20', ' ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<h1 class="step-title title">{$t('email_sent')}</h1>
|
||||
<h4 class="step-subtitle normal">
|
||||
{$t(message)}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,75 +1,77 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { formatError } from "$lib/utils/helpers";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { base } from '$app/paths';
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { formatError } from '$lib/utils/helpers';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals }) {
|
||||
// redirect user if logged in
|
||||
if (locals.user) {
|
||||
throw redirect(302, "/");
|
||||
}
|
||||
// redirect user if logged in
|
||||
console.log('loading login page');
|
||||
if (locals.user) {
|
||||
console.log('user is logged in');
|
||||
throw redirect(302, `${base}/auth/about/${locals.user.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
login: async ({ request, fetch, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const email = String(data.get("email"));
|
||||
const password = String(data.get("password"));
|
||||
const next = String(data.get("next"));
|
||||
/**
|
||||
*
|
||||
* @param request - The request object
|
||||
* @param fetch - Fetch object from sveltekit
|
||||
* @param cookies - SvelteKit's cookie object
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
login: async ({ request, fetch, cookies }) => {
|
||||
console.log('login action called');
|
||||
const data = await request.formData();
|
||||
const email = String(data.get('email'));
|
||||
const password = String(data.get('password'));
|
||||
const next = String(data.get('next'));
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
};
|
||||
console.log('API call url:', `${BASE_API_URI}/users/login`);
|
||||
const res = await fetch(`${BASE_API_URI}/users/login`, requestInitOptions);
|
||||
console.log('Login response status:', res.status);
|
||||
console.log('Login response headers:', Object.fromEntries(res.headers));
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
};
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
const errors = formatError(errorData.errors);
|
||||
return fail(res.status, { errors });
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_API_URI}/users/login/`, requestInitOptions);
|
||||
const responseBody = await res.json();
|
||||
console.log('Login response body:', responseBody);
|
||||
|
||||
console.log("Login response status:", res.status);
|
||||
console.log("Login response headers:", Object.fromEntries(res.headers));
|
||||
// Extract the JWT from the response headers
|
||||
const setCookieHeader = res.headers.get('set-cookie');
|
||||
if (setCookieHeader) {
|
||||
const jwtMatch = setCookieHeader.match(/jwt=([^;]+)/);
|
||||
if (jwtMatch) {
|
||||
const jwtValue = jwtMatch[1];
|
||||
// Set the cookie for the client
|
||||
cookies.set('jwt', jwtValue, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production', // Secure in production
|
||||
sameSite: 'lax',
|
||||
maxAge: 5 * 24 * 60 * 60 // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
const errors = formatError(errorData.errors);
|
||||
return fail(res.status, { errors });
|
||||
}
|
||||
|
||||
const responseBody = await res.json();
|
||||
console.log("Login response body:", responseBody);
|
||||
|
||||
// Extract the JWT from the response headers
|
||||
const setCookieHeader = res.headers.get("set-cookie");
|
||||
if (setCookieHeader) {
|
||||
const jwtMatch = setCookieHeader.match(/jwt=([^;]+)/);
|
||||
if (jwtMatch) {
|
||||
const jwtValue = jwtMatch[1];
|
||||
// Set the cookie for the client
|
||||
cookies.set("jwt", jwtValue, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Redirecting to:", next || "/");
|
||||
throw redirect(303, next || "/");
|
||||
},
|
||||
console.log('Redirecting to:', next || `${base}/auth/about/${responseBody.user_id}`);
|
||||
throw redirect(303, next || `${base}/auth/about/${responseBody.user_id}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,94 +1,81 @@
|
||||
<script>
|
||||
import { applyAction, enhance } from "$app/forms";
|
||||
import { page } from "$app/stores";
|
||||
import { receive, send } from "$lib/utils/helpers";
|
||||
import { t } from "svelte-i18n";
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { receive, send } from '$lib/utils/helpers';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleLogin = async () => {
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleLogin = async () => {
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
|
||||
let message = "";
|
||||
if ($page.url.searchParams.get("message")) {
|
||||
message = $page.url.search.split("=")[1].replaceAll("%20", " ");
|
||||
}
|
||||
let message = '';
|
||||
if ($page.url.searchParams.get('message')) {
|
||||
message = $page.url.search.split('=')[1].replaceAll('%20', ' ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<form
|
||||
class="content"
|
||||
method="POST"
|
||||
action="?/login"
|
||||
use:enhance={handleLogin}
|
||||
>
|
||||
<h1 class="step-title">{$t("user.login")}</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive={{ key: error.id }}
|
||||
out:send={{ key: error.id }}
|
||||
>
|
||||
{$t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
<form class="content" method="POST" action="?/login" use:enhance={handleLogin}>
|
||||
<h1 class="step-title">{$t('user.login')}</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive|global={{ key: error.id }}
|
||||
out:send|global={{ key: error.id }}
|
||||
>
|
||||
{$t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if message}
|
||||
<h4 class="step-subtitle">{message}</h4>
|
||||
{/if}
|
||||
{#if message}
|
||||
<h4 class="step-subtitle">{$t(message)}</h4>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="next"
|
||||
value={$page.url.searchParams.get("next")}
|
||||
/>
|
||||
<div class="input-box">
|
||||
<span class="label">{$t("email")}:</span>
|
||||
<input
|
||||
class="input"
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="{$t('placeholder.email')} "
|
||||
/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<span class="label">{$t("password")}:</span>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={$t("placeholder.password")}
|
||||
/>
|
||||
<a href="/auth/password/request-change" class="forgot-password"
|
||||
>{$t("forgot_password")}?</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<button class="button-dark">{$t("login")} </button>
|
||||
</div>
|
||||
</form>
|
||||
<input type="hidden" name="next" value={$page.url.searchParams.get('next')} />
|
||||
<div class="input-box">
|
||||
<span class="label">{$t('user.email')}:</span>
|
||||
<input class="input" type="email" name="email" placeholder="{$t('placeholder.email')} " />
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<span class="label">{$t('password')}:</span>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={$t('placeholder.password')}
|
||||
/>
|
||||
<a href={`${base}/auth/password/change`} class="forgot-password">{$t('forgot_password')}?</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<button class="button-dark">{$t('login')} </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
}
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.forgot-password {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { base } from '$app/paths';
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals }) {
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `/auth/login?next=/`);
|
||||
}
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `${base}/auth/login?next=${base}/`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async ({ fetch, cookies }) => {
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
};
|
||||
default: async ({ fetch, cookies }) => {
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `jwt=${cookies.get('jwt')}`
|
||||
}
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
`${BASE_API_URI}/backend/users/logout/`,
|
||||
requestInitOptions
|
||||
);
|
||||
const res = await fetch(`${BASE_API_URI}/auth/logout/`, requestInitOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = [];
|
||||
errors.push({ error: response.error, id: 0 });
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = [];
|
||||
errors.push({ error: response.error, id: 0 });
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
// eat the cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
// eat the cookie
|
||||
cookies.delete('jwt', { path: '/' });
|
||||
|
||||
// The server should clear the cookie, so we don't need to handle it here
|
||||
// Just check if the cookie is cleared in the response
|
||||
const setCookieHeader = res.headers.get("set-cookie");
|
||||
if (!setCookieHeader || !setCookieHeader.includes("jwt=;")) {
|
||||
console.error("JWT cookie not cleared in response");
|
||||
return fail(500, {
|
||||
errors: [
|
||||
{
|
||||
error: "Server error: Failed to clear authentication token",
|
||||
id: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
// redirect the user
|
||||
throw redirect(302, "/auth/login");
|
||||
},
|
||||
// The server should clear the cookie, so we don't need to handle it here
|
||||
// Just check if the cookie is cleared in the response
|
||||
const setCookieHeader = res.headers.get('set-cookie');
|
||||
if (!setCookieHeader || !setCookieHeader.includes('jwt=;')) {
|
||||
console.error('JWT cookie not cleared in response');
|
||||
return fail(500, {
|
||||
errors: [
|
||||
{
|
||||
error: 'Server error: Failed to clear authentication token',
|
||||
id: Date.now()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
// redirect the user
|
||||
throw redirect(302, `${base}/auth/login`);
|
||||
}
|
||||
};
|
||||
|
||||
35
frontend/src/routes/auth/password/change/+page.server.js
Normal file
35
frontend/src/routes/auth/password/change/+page.server.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { base } from '$app/paths';
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { formatError } from '$lib/utils/helpers';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async ({ fetch, request }) => {
|
||||
const formData = await request.formData();
|
||||
const email = String(formData.get('email'));
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: email })
|
||||
};
|
||||
|
||||
const res = await fetch(`${BASE_API_URI}/users/password/request-change/`, requestInitOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
|
||||
// redirect the user
|
||||
console.log('redirecting to: ', `${base}/auth/confirming?message=${response.message}`);
|
||||
throw redirect(302, `${base}/auth/confirming?message=${response.message}`);
|
||||
}
|
||||
};
|
||||
53
frontend/src/routes/auth/password/change/+page.svelte
Normal file
53
frontend/src/routes/auth/password/change/+page.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import SmallLoader from '$lib/components/SmallLoader.svelte';
|
||||
import { receive, send } from '$lib/utils/helpers';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
let loading = false;
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleRequestChange = async () => {
|
||||
loading = true;
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
loading = false;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<form class="content" method="POST" use:enhance={handleRequestChange}>
|
||||
<h1 class="step-title">{$t('forgot_password')}</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive={{ key: error.id }}
|
||||
out:send={{ key: error.id }}
|
||||
>
|
||||
{$t(error.key)}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="input-box">
|
||||
<span class="label">{$t('user.email')}:</span>
|
||||
<input
|
||||
class="input"
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder={$t('placeholder.email')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if loading}
|
||||
<SmallLoader width={30} message={$t('loading.please_wait')} />
|
||||
{:else}
|
||||
<button class="button-dark" disabled={loading}>{$t('confirm')}</button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { base } from '$app/paths';
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { formatError } from '$lib/utils/helpers';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
export const actions = {
|
||||
default: async ({ fetch, request }) => {
|
||||
const formData = await request.formData();
|
||||
const password = String(formData.get('user[password]')).trim();
|
||||
const confirmPassword = String(formData.get('confirm_password')).trim();
|
||||
let token = String(formData.get('token'));
|
||||
const userID = String(formData.get('user_id'));
|
||||
|
||||
// Some validations
|
||||
/** @type {string | Array<{field: string, key: string}> | Record<string, {key: string}>} */
|
||||
const fieldsError = [];
|
||||
if (password.length < 8) {
|
||||
fieldsError.push({ field: 'user.user', key: 'validation.password' });
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
fieldsError.push({ field: 'user.user', key: 'validation.password_match' });
|
||||
}
|
||||
if (Object.keys(fieldsError).length > 0) {
|
||||
return fail(400, { errors: formatError(fieldsError) });
|
||||
}
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestInitOptions = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ token: token, password: password })
|
||||
};
|
||||
|
||||
const res = await fetch(`${BASE_API_URI}/users/password/change/${userID}/`, requestInitOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
|
||||
// redirect the user
|
||||
console.log('redirecting to: ', `${base}/auth/login?message=${response.message}`);
|
||||
throw redirect(302, `${base}/auth/login?message=${response.message}`);
|
||||
}
|
||||
};
|
||||
84
frontend/src/routes/auth/password/change/[id]/+page.svelte
Normal file
84
frontend/src/routes/auth/password/change/[id]/+page.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<script>
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { receive, send } from '$lib/utils/helpers';
|
||||
|
||||
import { t } from 'svelte-i18n';
|
||||
import InputField from '$lib/components/InputField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let password = '',
|
||||
confirm_password = '';
|
||||
|
||||
/** @type{string | null} */
|
||||
let token = null;
|
||||
|
||||
onMount(() => {
|
||||
token = page.url.searchParams.get('token');
|
||||
console.log(token);
|
||||
if (!token) {
|
||||
form ||= { errors: [] }; // Ensure form exists with an errors array
|
||||
form.errors.push({
|
||||
field: 'server.general',
|
||||
key: 'server.error.no_auth_token',
|
||||
id: Math.random() * 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleChange = async () => {
|
||||
return async ({ result }) => {
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<form class="content" method="POST" use:enhance={handleChange}>
|
||||
<h1 class="step-title title">{$t('change_password')}</h1>
|
||||
{#if form?.errors}
|
||||
{#each form.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive={{ key: error.id }}
|
||||
out:send={{ key: error.id }}
|
||||
>
|
||||
{$t(error.key, {
|
||||
values: {
|
||||
message:
|
||||
error.field
|
||||
.split(' ')
|
||||
.map((tag) => $t(tag))
|
||||
.join(', ') || ''
|
||||
}
|
||||
})}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<input type="hidden" name="user_id" value={page.params.id} />
|
||||
<input type="hidden" name="token" value={token} />
|
||||
<InputField
|
||||
name="user[password]"
|
||||
type="password"
|
||||
label={$t('password')}
|
||||
placeholder={$t('placeholder.password')}
|
||||
bind:value={password}
|
||||
otherPasswordValue={confirm_password}
|
||||
/>
|
||||
<InputField
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
label={$t('confirm_password')}
|
||||
placeholder={$t('placeholder.password')}
|
||||
bind:value={confirm_password}
|
||||
otherPasswordValue={password}
|
||||
/>
|
||||
|
||||
<button class="button-dark">{$t('change_password')} </button>
|
||||
</form>
|
||||
</div>
|
||||
5002
frontend/src/static/css/bootstrap-grid.css
vendored
5002
frontend/src/static/css/bootstrap-grid.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
426
frontend/src/static/css/bootstrap-reboot.css
vendored
426
frontend/src/static/css/bootstrap-reboot.css
vendored
@@ -1,426 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
File diff suppressed because one or more lines are too long
4752
frontend/src/static/css/bootstrap-utilities.css
vendored
4752
frontend/src/static/css/bootstrap-utilities.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10837
frontend/src/static/css/bootstrap.css
vendored
10837
frontend/src/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
frontend/src/static/css/bootstrap.min.css
vendored
7
frontend/src/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6748
frontend/src/static/js/bootstrap.bundle.js
vendored
6748
frontend/src/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4967
frontend/src/static/js/bootstrap.esm.js
vendored
4967
frontend/src/static/js/bootstrap.esm.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
frontend/src/static/js/bootstrap.esm.min.js
vendored
7
frontend/src/static/js/bootstrap.esm.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5016
frontend/src/static/js/bootstrap.js
vendored
5016
frontend/src/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
frontend/src/static/js/bootstrap.min.js
vendored
7
frontend/src/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,15 +1,17 @@
|
||||
// import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-vercel';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({
|
||||
runtime: 'edge'
|
||||
})
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
paths: {
|
||||
base: isProduction ? '/backend' : ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
|
||||
@@ -18,18 +18,18 @@ func main() {
|
||||
|
||||
config.LoadConfig()
|
||||
|
||||
err := database.Open(config.DB.Path, config.Recipients.AdminEmail)
|
||||
db, err := database.Open(config.DB.Path, config.Recipients.AdminEmail, config.Env == "development")
|
||||
if err != nil {
|
||||
logger.Error.Fatalf("Couldn't init database: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := database.Close(); err != nil {
|
||||
if err := database.Close(db); err != nil {
|
||||
logger.Error.Fatalf("Failed to close database: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go server.Run()
|
||||
go server.Run(db)
|
||||
|
||||
gracefulShutdown()
|
||||
}
|
||||
10
go-backend/compose.yml
Normal file
10
go-backend/compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: carsharingBackend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./go-backend/configs/config.json:/root/configs/config.json:ro
|
||||
- ./go-backend/data/db.sqlite3:/root/data/db.sqlite3
|
||||
- ./go-backend/templates:/root/templates:ro
|
||||
@@ -2,6 +2,7 @@
|
||||
"site": {
|
||||
"WebsiteTitle": "My Carsharing Site",
|
||||
"BaseUrl": "https://domain.de",
|
||||
"FrontendPath": "",
|
||||
"AllowOrigins": "https://domain.de"
|
||||
},
|
||||
"Environment": "dev",
|
||||
@@ -18,7 +19,7 @@
|
||||
"MailPath": "templates/email",
|
||||
"HTMLPath": "templates/html",
|
||||
"StaticPath": "templates/css",
|
||||
"LogoURI": "/images/LOGO.png"
|
||||
"LogoURI": "/assets/LOGO.png"
|
||||
},
|
||||
"auth": {
|
||||
"APIKey": ""
|
||||
1
go-backend/data/README.md
Normal file
1
go-backend/data/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Database Folder according to the project structure and the template configuration
|
||||
@@ -12,16 +12,21 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.3.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
|
||||
github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
@@ -28,9 +30,13 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jbub/banking v0.8.0 h1:79kXJj1X2E9dWdWuFNkk2Pw7c6uYPFQS8ev0l+zMFxk=
|
||||
github.com/jbub/banking v0.8.0/go.mod h1:ctv/bD2EGRR5PobFrJSXZ/FZXCFtUbmVv6v2qf/b/88=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -85,21 +91,60 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
@@ -8,6 +8,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -15,7 +17,6 @@ import (
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ type SiteConfig struct {
|
||||
AllowOrigins string `json:"AllowOrigins" envconfig:"ALLOW_ORIGINS"`
|
||||
WebsiteTitle string `json:"WebsiteTitle" envconfig:"WEBSITE_TITLE"`
|
||||
BaseURL string `json:"BaseUrl" envconfig:"BASE_URL"`
|
||||
FrontendPath string `json:"FrontendPath" envconfig:"FRONTEND_PATH"`
|
||||
}
|
||||
type AuthenticationConfig struct {
|
||||
JWTSecret string
|
||||
@@ -60,6 +62,16 @@ type SecurityConfig struct {
|
||||
Burst int `json:"Burst" default:"60" envconfig:"BURST_LIMIT"`
|
||||
} `json:"RateLimits"`
|
||||
}
|
||||
|
||||
type CompanyConfig struct {
|
||||
Name string `json:"Name" envconfig:"COMPANY_NAME"`
|
||||
Address string `json:"Address" envconfig:"COMPANY_ADDRESS"`
|
||||
City string `json:"City" envconfig:"COMPANY_CITY"`
|
||||
ZipCode string `json:"ZipCode" envconfig:"COMPANY_ZIPCODE"`
|
||||
Country string `json:"Country" envconfig:"COMPANY_COUNTRY"`
|
||||
SepaPrefix string `json:"SepaPrefix" envconfig:"COMPANY_SEPA_PREFIX"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth AuthenticationConfig `json:"auth"`
|
||||
Site SiteConfig `json:"site"`
|
||||
@@ -70,6 +82,7 @@ type Config struct {
|
||||
DB DatabaseConfig `json:"db"`
|
||||
SMTP SMTPConfig `json:"smtp"`
|
||||
Security SecurityConfig `json:"security"`
|
||||
Company CompanyConfig `json:"company"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -83,7 +96,9 @@ var (
|
||||
Recipients RecipientsConfig
|
||||
Env string
|
||||
Security SecurityConfig
|
||||
Company CompanyConfig
|
||||
)
|
||||
|
||||
var environmentOptions map[string]bool = map[string]bool{
|
||||
"development": true,
|
||||
"production": true,
|
||||
@@ -95,15 +110,15 @@ var environmentOptions map[string]bool = map[string]bool{
|
||||
// It also generates JWT and CSRF secrets. Returns a Config pointer or an error if any step fails.
|
||||
func LoadConfig() {
|
||||
CFGPath = os.Getenv("CONFIG_FILE_PATH")
|
||||
logger.Info.Printf("Config file environment: %v", CFGPath)
|
||||
readFile(&CFG)
|
||||
readEnv(&CFG)
|
||||
csrfSecret, err := utils.GenerateRandomString(32)
|
||||
logger.Info.Printf("Config file environment: %v", CFGPath)
|
||||
csrfSecret, err := generateRandomString(32)
|
||||
if err != nil {
|
||||
logger.Error.Fatalf("could not generate CSRF secret: %v", err)
|
||||
}
|
||||
|
||||
jwtSecret, err := utils.GenerateRandomString(32)
|
||||
jwtSecret, err := generateRandomString(32)
|
||||
if err != nil {
|
||||
logger.Error.Fatalf("could not generate JWT secret: %v", err)
|
||||
}
|
||||
@@ -122,6 +137,7 @@ func LoadConfig() {
|
||||
Security = CFG.Security
|
||||
Env = CFG.Env
|
||||
Site = CFG.Site
|
||||
Company = CFG.Company
|
||||
logger.Info.Printf("Config loaded: %#v", CFG)
|
||||
}
|
||||
|
||||
@@ -159,3 +175,12 @@ func readEnv(cfg *Config) {
|
||||
logger.Error.Fatalf("could not decode env variables: %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomString(length int) (string, error) {
|
||||
bytes := make([]byte, length)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
109
go-backend/internal/constants/constants.go
Normal file
109
go-backend/internal/constants/constants.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
UnverifiedStatus = iota + 1
|
||||
DisabledStatus
|
||||
VerifiedStatus
|
||||
ActiveStatus
|
||||
PassiveStatus
|
||||
DelayedPaymentStatus
|
||||
SettledPaymentStatus
|
||||
AwaitingPaymentStatus
|
||||
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
|
||||
MailChangePasswordSubject = "Passwort Änderung angefordert"
|
||||
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
|
||||
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
|
||||
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
|
||||
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
|
||||
SupporterSubscriptionName = "Keins"
|
||||
)
|
||||
|
||||
var Licences = struct {
|
||||
AM string
|
||||
A1 string
|
||||
A2 string
|
||||
A string
|
||||
B string
|
||||
C1 string
|
||||
C string
|
||||
D1 string
|
||||
D string
|
||||
BE string
|
||||
C1E string
|
||||
CE string
|
||||
D1E string
|
||||
DE string
|
||||
L string
|
||||
T string
|
||||
}{
|
||||
AM: "AM",
|
||||
A1: "A1",
|
||||
A2: "A2",
|
||||
A: "A",
|
||||
B: "B",
|
||||
C1: "C1",
|
||||
C: "C",
|
||||
D1: "D1",
|
||||
D: "D",
|
||||
BE: "BE",
|
||||
C1E: "C1E",
|
||||
CE: "CE",
|
||||
D1E: "D1E",
|
||||
DE: "DE",
|
||||
L: "L",
|
||||
T: "T",
|
||||
}
|
||||
|
||||
var VerificationTypes = struct {
|
||||
Email string
|
||||
Password string
|
||||
}{
|
||||
Email: "email",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
var Priviliges = struct {
|
||||
View int8
|
||||
Create int8
|
||||
Update int8
|
||||
Delete int8
|
||||
AccessControl int8
|
||||
}{
|
||||
View: 2,
|
||||
Update: 4,
|
||||
Create: 4,
|
||||
Delete: 4,
|
||||
AccessControl: 8,
|
||||
}
|
||||
|
||||
var Roles = struct {
|
||||
Opponent int8
|
||||
Supporter int8
|
||||
Member int8
|
||||
Viewer int8
|
||||
Editor int8
|
||||
Admin int8
|
||||
}{
|
||||
Opponent: -5,
|
||||
Supporter: 0,
|
||||
Member: 1,
|
||||
Viewer: 2,
|
||||
Editor: 4,
|
||||
Admin: 8,
|
||||
}
|
||||
|
||||
var MemberUpdateFields = map[string]bool{
|
||||
"Email": true,
|
||||
"Phone": true,
|
||||
"Company": true,
|
||||
"Address": true,
|
||||
"ZipCode": true,
|
||||
"City": true,
|
||||
"Licence.Categories": true,
|
||||
"BankAccount.Bank": true,
|
||||
"BankAccount.AccountHolderName": true,
|
||||
"BankAccount.IBAN": true,
|
||||
"BankAccount.BIC": true,
|
||||
}
|
||||
|
||||
// "Password": true,
|
||||
116
go-backend/internal/controllers/car_controller.go
Normal file
116
go-backend/internal/controllers/car_controller.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CarController struct {
|
||||
S services.CarServiceInterface
|
||||
UserService services.UserServiceInterface
|
||||
}
|
||||
|
||||
func (cr *CarController) Create(c *gin.Context) {
|
||||
requestUser, err := cr.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in Create car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Create) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to create a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Create), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
var newCar models.Car
|
||||
if err := c.ShouldBindJSON(&newCar); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
car, err := cr.S.Create(&newCar)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error creating car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, car)
|
||||
}
|
||||
|
||||
func (cr *CarController) Update(c *gin.Context) {
|
||||
requestUser, err := cr.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in Update car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Update) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to update a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Update), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
var car models.Car
|
||||
if err := c.ShouldBindJSON(&car); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
logger.Error.Printf("updating car: %v", car)
|
||||
updatedCar, err := cr.S.Update(&car)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error updating car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, updatedCar)
|
||||
}
|
||||
|
||||
func (cr *CarController) GetAll(c *gin.Context) {
|
||||
requestUser, err := cr.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in GetAll car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.View) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to access car data. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Delete), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
cars, err := cr.S.GetAll()
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error getting cars", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"cars": cars,
|
||||
})
|
||||
}
|
||||
|
||||
func (cr *CarController) Delete(c *gin.Context) {
|
||||
type input struct {
|
||||
ID uint `json:"id" binding:"required,numeric"`
|
||||
}
|
||||
var deleteData input
|
||||
requestUser, err := cr.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in Delete car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Delete) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to delete a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Delete), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&deleteData); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
err = cr.S.Delete(&deleteData.ID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error deleting car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, "Car deleted")
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"GoMembership/internal/config"
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/database"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
@@ -44,12 +45,14 @@ type loginInput struct {
|
||||
}
|
||||
|
||||
var (
|
||||
Uc *UserController
|
||||
Mc *MembershipController
|
||||
Cc *ContactController
|
||||
Uc *UserController
|
||||
Mc *MembershipController
|
||||
Cc *ContactController
|
||||
AdminCookie *http.Cookie
|
||||
MemberCookie *http.Cookie
|
||||
)
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
func TestMain(t *testing.T) {
|
||||
_ = deleteTestDB("test.db")
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
@@ -84,7 +87,8 @@ func TestSuite(t *testing.T) {
|
||||
log.Fatalf("Error setting environment variable: %v", err)
|
||||
}
|
||||
config.LoadConfig()
|
||||
if err := database.Open("test.db", config.Recipients.AdminEmail); err != nil {
|
||||
db, err := database.Open("test.db", config.Recipients.AdminEmail, true)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create DB: %#v", err)
|
||||
}
|
||||
utils.SMTPStart(Host, Port)
|
||||
@@ -96,17 +100,16 @@ func TestSuite(t *testing.T) {
|
||||
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
|
||||
|
||||
var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{}
|
||||
var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{}
|
||||
var subscriptionRepo repositories.SubscriptionsRepositoryInterface = &repositories.SubscriptionsRepository{}
|
||||
membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo}
|
||||
|
||||
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
|
||||
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
|
||||
userService := &services.UserService{Repo: userRepo, Licences: licenceRepo}
|
||||
userService := &services.UserService{DB: db, Licences: licenceRepo}
|
||||
|
||||
licenceService := &services.LicenceService{Repo: licenceRepo}
|
||||
|
||||
Uc = &UserController{Service: userService, LicenceService: licenceService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
|
||||
Mc = &MembershipController{Service: *membershipService}
|
||||
Mc = &MembershipController{UserService: userService, Service: membershipService}
|
||||
Cc = &ContactController{EmailService: emailService}
|
||||
|
||||
if err := initSubscriptionPlans(); err != nil {
|
||||
@@ -116,11 +119,36 @@ func TestSuite(t *testing.T) {
|
||||
if err := initLicenceCategories(); err != nil {
|
||||
log.Fatalf("Failed to init Categories: %v", err)
|
||||
}
|
||||
validation.SetupValidators()
|
||||
password := "securepassword"
|
||||
admin := models.User{
|
||||
FirstName: "Ad",
|
||||
LastName: "min",
|
||||
Email: "admin@example.com",
|
||||
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Company: "SampleCorp",
|
||||
Phone: "+123456789",
|
||||
Address: "123 Main Street",
|
||||
ZipCode: "12345",
|
||||
City: "SampleCity",
|
||||
Status: constants.ActiveStatus,
|
||||
Password: password,
|
||||
Notes: "",
|
||||
RoleID: constants.Roles.Admin,
|
||||
Consents: nil,
|
||||
Verifications: nil,
|
||||
Membership: nil,
|
||||
BankAccount: nil,
|
||||
Licence: &models.Licence{
|
||||
Status: constants.UnverifiedStatus,
|
||||
}}
|
||||
admin.Create(db)
|
||||
validation.SetupValidators(db)
|
||||
t.Run("userController", func(t *testing.T) {
|
||||
testUserController(t)
|
||||
})
|
||||
t.Run("Password_Controller", func(t *testing.T) {
|
||||
|
||||
})
|
||||
t.Run("SQL_Injection", func(t *testing.T) {
|
||||
testSQLInjectionAttempt(t)
|
||||
})
|
||||
@@ -136,7 +164,6 @@ func TestSuite(t *testing.T) {
|
||||
t.Run("XSSAttempt", func(t *testing.T) {
|
||||
testXSSAttempt(t)
|
||||
})
|
||||
|
||||
if err := utils.SMTPStop(); err != nil {
|
||||
log.Fatalf("Failed to stop SMTP Mockup Server: %#v", err)
|
||||
}
|
||||
@@ -176,7 +203,7 @@ func initLicenceCategories() error {
|
||||
}
|
||||
|
||||
func initSubscriptionPlans() error {
|
||||
subscriptions := []models.SubscriptionModel{
|
||||
subscriptions := []models.Subscription{
|
||||
{
|
||||
Name: "Basic",
|
||||
Details: "Test Plan",
|
||||
@@ -248,23 +275,41 @@ func GetMockedFormContext(formData url.Values, url string) (*gin.Context, *httpt
|
||||
|
||||
func getBaseUser() models.User {
|
||||
return models.User{
|
||||
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john.doe@example.com",
|
||||
Address: "Pablo Escobar Str. 4",
|
||||
ZipCode: "25474",
|
||||
City: "Hasloh",
|
||||
Phone: "01738484993",
|
||||
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"},
|
||||
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
|
||||
Licence: nil,
|
||||
ProfilePicture: "",
|
||||
Password: "password123",
|
||||
Company: "",
|
||||
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
Email: "john.doe@example.com",
|
||||
Address: "Pablo Escobar Str. 4",
|
||||
ZipCode: "25474",
|
||||
City: "Hasloh",
|
||||
Phone: "01738484993",
|
||||
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
|
||||
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
|
||||
Licence: nil,
|
||||
Password: "passw@#$#%$!-ord123",
|
||||
Company: "",
|
||||
RoleID: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func getBaseSupporter() models.User {
|
||||
return models.User{
|
||||
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
FirstName: "John",
|
||||
LastName: "Rich",
|
||||
Email: "john.supporter@example.com",
|
||||
Address: "Pablo Escobar Str. 4",
|
||||
ZipCode: "25474",
|
||||
City: "Hasloh",
|
||||
Phone: "01738484993",
|
||||
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
|
||||
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
|
||||
Licence: nil,
|
||||
Password: "passw@#$#%$!-ord123",
|
||||
Company: "",
|
||||
RoleID: 0,
|
||||
}
|
||||
}
|
||||
func deleteTestDB(dbPath string) error {
|
||||
err := os.Remove(dbPath)
|
||||
if err != nil {
|
||||
@@ -3,13 +3,14 @@ package controllers
|
||||
import (
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LicenceController struct {
|
||||
Service services.LicenceService
|
||||
Service services.LicenceServiceInterface
|
||||
}
|
||||
|
||||
func (lc *LicenceController) GetAllCategories(c *gin.Context) {
|
||||
@@ -17,7 +18,7 @@ func (lc *LicenceController) GetAllCategories(c *gin.Context) {
|
||||
categories, err := lc.Service.GetAllCategories()
|
||||
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error retrieving licence categories", http.StatusInternalServerError, "general", "server.error.internal_server_error")
|
||||
utils.RespondWithError(c, err, "Error retrieving licence categories", http.StatusInternalServerError, errors.Responses.Fields.Licences, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
94
go-backend/internal/controllers/licenceController_test.go
Normal file
94
go-backend/internal/controllers/licenceController_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Mock Repository
|
||||
type MockLicenceRepo struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockLicenceRepo) GetAllCategories() ([]models.Category, error) {
|
||||
args := m.Called()
|
||||
categories, _ := args.Get(0).([]models.Category) // Safe type assertion
|
||||
return categories, args.Error(1)
|
||||
}
|
||||
|
||||
func (r *MockLicenceRepo) FindCategoriesByIDs(ids []uint) ([]models.Category, error) {
|
||||
return []models.Category{}, nil
|
||||
}
|
||||
|
||||
func (r *MockLicenceRepo) FindCategoryByName(categoryName string) (models.Category, error) {
|
||||
return models.Category{}, nil
|
||||
}
|
||||
|
||||
func TestGetAllCategories_Success(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// Mock repository
|
||||
mockRepo := new(MockLicenceRepo)
|
||||
expectedCategories := []models.Category{
|
||||
{ID: 1, Name: "Category A"},
|
||||
{ID: 2, Name: "Category B"},
|
||||
}
|
||||
mockRepo.On("GetAllCategories").Return(expectedCategories, nil)
|
||||
|
||||
// Create LicenceService with mocked repository
|
||||
service := &services.LicenceService{Repo: mockRepo}
|
||||
|
||||
// Create controller with service
|
||||
lc := &LicenceController{Service: service}
|
||||
|
||||
// Setup router and request
|
||||
router := gin.Default()
|
||||
router.GET("/licence/categories", lc.GetAllCategories)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/licence/categories", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.JSONEq(t, `{"licence_categories":[{"id":1,"category":"Category A"},{"id":2,"category":"Category B"}]}`, w.Body.String())
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGetAllCategories_Error(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
// Mock repository
|
||||
mockRepo := new(MockLicenceRepo)
|
||||
mockRepo.On("GetAllCategories").Return(nil, errors.New("database error"))
|
||||
|
||||
// Create LicenceService with mocked repository
|
||||
service := &services.LicenceService{Repo: mockRepo}
|
||||
|
||||
// Create controller with service
|
||||
lc := &LicenceController{Service: service}
|
||||
|
||||
// Setup router and request
|
||||
router := gin.Default()
|
||||
router.GET("/licence/categories", lc.GetAllCategories)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/licence/categories", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "server.error.internal_server_error")
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
132
go-backend/internal/controllers/membershipController.go
Normal file
132
go-backend/internal/controllers/membershipController.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/services"
|
||||
"GoMembership/internal/utils"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
)
|
||||
|
||||
type MembershipController struct {
|
||||
Service services.MembershipServiceInterface
|
||||
UserService services.UserServiceInterface
|
||||
}
|
||||
|
||||
func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
|
||||
|
||||
requestUser, err := mc.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription registrationHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Create) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to register subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var subscription models.Subscription
|
||||
if err := c.ShouldBindJSON(&subscription); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Register Subscription
|
||||
id, err := mc.Service.RegisterSubscription(&subscription)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
utils.RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.Subscription, errors.Responses.Keys.Duplicate)
|
||||
} else {
|
||||
utils.RespondWithError(c, err, "Couldn't register Membershipmodel", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
logger.Info.Printf("registering subscription: %+v", subscription)
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"status": "success",
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) UpdateHandler(c *gin.Context) {
|
||||
|
||||
requestUser, err := mc.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription Updatehandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Update) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var subscription models.Subscription
|
||||
if err := c.ShouldBindJSON(&subscription); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// update Subscription
|
||||
logger.Info.Printf("Updating subscription %v", subscription.Name)
|
||||
id, err := mc.Service.UpdateSubscription(&subscription)
|
||||
if err != nil {
|
||||
utils.HandleSubscriptionUpdateError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"status": "success",
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
|
||||
type deleteData struct {
|
||||
ID uint `json:"id" binding:"required,numeric,safe_content"`
|
||||
Name string `json:"name" binding:"required,safe_content"`
|
||||
}
|
||||
|
||||
var subscription deleteData
|
||||
requestUser, err := mc.UserService.FromContext(c)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error extracting user from context in subscription deleteSubscription", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
|
||||
return
|
||||
}
|
||||
|
||||
if !requestUser.HasPrivilege(constants.Priviliges.Delete) {
|
||||
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&subscription); err != nil {
|
||||
utils.HandleValidationError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := mc.Service.DeleteSubscription(&subscription.ID, &subscription.Name); err != nil {
|
||||
utils.HandleSubscriptionDeleteError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Subscription deleted successfully"})
|
||||
}
|
||||
|
||||
func (mc *MembershipController) GetSubscriptions(c *gin.Context) {
|
||||
subscriptions, err := mc.Service.GetSubscriptions(nil)
|
||||
if err != nil {
|
||||
utils.RespondWithError(c, err, "Error retrieving subscriptions", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"subscriptions": subscriptions,
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user