mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 20:06:43 +02:00
Feat: New project chat, uload refactor (postgress), hosting plans, uload landingpage
This commit is contained in:
parent
559eb08d8c
commit
fcf3a344b1
123 changed files with 7106 additions and 3715 deletions
|
|
@ -58,6 +58,8 @@
|
|||
"zod": "^4.0.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@aws-sdk/client-s3": "^3.934.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.934.0",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
|
|
|
|||
1
uload/apps/web/project.inlang/.gitignore
vendored
1
uload/apps/web/project.inlang/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
cache
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"nav_login": "Anmelden",
|
||||
"nav_register": "Registrieren",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_folders": "Ordner",
|
||||
"nav_profile": "Profil",
|
||||
"nav_logout": "Abmelden",
|
||||
"home_title": "Links intelligenter teilen",
|
||||
"home_subtitle": "Erstelle verkürzte Links mit QR-Codes, benutzerdefinierten Namen und Analysen",
|
||||
"home_url_label_qr": "URL zum Kodieren",
|
||||
"home_url_label": "URL zum Kürzen",
|
||||
"home_title_label": "Titel",
|
||||
"home_title_placeholder": "Gib deinem Link einen Namen",
|
||||
"home_description_label": "Beschreibung",
|
||||
"home_description_placeholder": "Füge eine Beschreibung hinzu (optional)",
|
||||
"home_expires_label": "Ablauf",
|
||||
"home_expires_placeholder": "z.B. 7 Tage, 1 Monat",
|
||||
"home_max_clicks_label": "Max. Klicks",
|
||||
"home_max_clicks_placeholder": "Anzahl der Klicks begrenzen",
|
||||
"home_password_label": "Passwort",
|
||||
"home_password_placeholder": "Mit Passwort schützen",
|
||||
"home_guest_info": "Du verwendest uload als Gast",
|
||||
"auth_modal_signin": "Anmelden",
|
||||
"home_guest_signin_hint": "um auf erweiterte Funktionen zuzugreifen",
|
||||
"home_processing": "Verarbeitung...",
|
||||
"home_submit_button_qr": "QR-Code generieren",
|
||||
"home_submit_button": "Link erstellen"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"nav_login": "Login",
|
||||
"nav_register": "Register",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_folders": "Folders",
|
||||
"nav_profile": "Profile",
|
||||
"nav_logout": "Logout",
|
||||
"home_title": "Share Links Smarter",
|
||||
"home_subtitle": "Create shortened links with QR codes, custom names, and analytics",
|
||||
"home_url_label_qr": "URL to encode",
|
||||
"home_url_label": "URL to shorten",
|
||||
"home_title_label": "Title",
|
||||
"home_title_placeholder": "Give your link a name",
|
||||
"home_description_label": "Description",
|
||||
"home_description_placeholder": "Add a description (optional)",
|
||||
"home_expires_label": "Expiration",
|
||||
"home_expires_placeholder": "e.g., 7 days, 1 month",
|
||||
"home_max_clicks_label": "Max clicks",
|
||||
"home_max_clicks_placeholder": "Limit number of clicks",
|
||||
"home_password_label": "Password",
|
||||
"home_password_placeholder": "Protect with password",
|
||||
"home_guest_info": "You're using uload as a guest",
|
||||
"auth_modal_signin": "Sign in",
|
||||
"home_guest_signin_hint": "to access advanced features",
|
||||
"home_processing": "Processing...",
|
||||
"home_submit_button_qr": "Generate QR Code",
|
||||
"home_submit_button": "Create Link"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"nav_login": "Iniciar sesión",
|
||||
"nav_register": "Registrarse",
|
||||
"nav_dashboard": "Panel",
|
||||
"nav_folders": "Carpetas",
|
||||
"nav_profile": "Perfil",
|
||||
"nav_logout": "Cerrar sesión",
|
||||
"home_title": "Comparte Enlaces de Forma Inteligente",
|
||||
"home_subtitle": "Crea enlaces acortados con códigos QR, nombres personalizados y análisis",
|
||||
"home_url_label_qr": "URL para codificar",
|
||||
"home_url_label": "URL para acortar",
|
||||
"home_title_label": "Título",
|
||||
"home_title_placeholder": "Dale un nombre a tu enlace",
|
||||
"home_description_label": "Descripción",
|
||||
"home_description_placeholder": "Añadir una descripción (opcional)",
|
||||
"home_expires_label": "Vencimiento",
|
||||
"home_expires_placeholder": "ej., 7 días, 1 mes",
|
||||
"home_max_clicks_label": "Clics máximos",
|
||||
"home_max_clicks_placeholder": "Limitar número de clics",
|
||||
"home_password_label": "Contraseña",
|
||||
"home_password_placeholder": "Proteger con contraseña",
|
||||
"home_guest_info": "Estás usando uload como invitado",
|
||||
"auth_modal_signin": "Iniciar sesión",
|
||||
"home_guest_signin_hint": "para acceder a funciones avanzadas",
|
||||
"home_processing": "Procesando...",
|
||||
"home_submit_button_qr": "Generar Código QR",
|
||||
"home_submit_button": "Crear Enlace"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"nav_login": "Connexion",
|
||||
"nav_register": "S'inscrire",
|
||||
"nav_dashboard": "Tableau de bord",
|
||||
"nav_folders": "Dossiers",
|
||||
"nav_profile": "Profil",
|
||||
"nav_logout": "Déconnexion",
|
||||
"home_title": "Partagez des Liens Intelligemment",
|
||||
"home_subtitle": "Créez des liens raccourcis avec codes QR, noms personnalisés et analyses",
|
||||
"home_url_label_qr": "URL à encoder",
|
||||
"home_url_label": "URL à raccourcir",
|
||||
"home_title_label": "Titre",
|
||||
"home_title_placeholder": "Donnez un nom à votre lien",
|
||||
"home_description_label": "Description",
|
||||
"home_description_placeholder": "Ajouter une description (optionnel)",
|
||||
"home_expires_label": "Expiration",
|
||||
"home_expires_placeholder": "ex., 7 jours, 1 mois",
|
||||
"home_max_clicks_label": "Clics maximum",
|
||||
"home_max_clicks_placeholder": "Limiter le nombre de clics",
|
||||
"home_password_label": "Mot de passe",
|
||||
"home_password_placeholder": "Protéger avec mot de passe",
|
||||
"home_guest_info": "Vous utilisez uload en tant qu'invité",
|
||||
"auth_modal_signin": "Se connecter",
|
||||
"home_guest_signin_hint": "pour accéder aux fonctionnalités avancées",
|
||||
"home_processing": "Traitement...",
|
||||
"home_submit_button_qr": "Générer Code QR",
|
||||
"home_submit_button": "Créer Lien"
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"nav_login": "Accedi",
|
||||
"nav_register": "Registrati",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_folders": "Cartelle",
|
||||
"nav_profile": "Profilo",
|
||||
"nav_logout": "Esci",
|
||||
"home_title": "Condividi Link in Modo Intelligente",
|
||||
"home_subtitle": "Crea link abbreviati con codici QR, nomi personalizzati e analisi",
|
||||
"home_url_label_qr": "URL da codificare",
|
||||
"home_url_label": "URL da abbreviare",
|
||||
"home_title_label": "Titolo",
|
||||
"home_title_placeholder": "Dai un nome al tuo link",
|
||||
"home_description_label": "Descrizione",
|
||||
"home_description_placeholder": "Aggiungi una descrizione (opzionale)",
|
||||
"home_expires_label": "Scadenza",
|
||||
"home_expires_placeholder": "es., 7 giorni, 1 mese",
|
||||
"home_max_clicks_label": "Click massimi",
|
||||
"home_max_clicks_placeholder": "Limita il numero di click",
|
||||
"home_password_label": "Password",
|
||||
"home_password_placeholder": "Proteggi con password",
|
||||
"home_guest_info": "Stai usando uload come ospite",
|
||||
"auth_modal_signin": "Accedi",
|
||||
"home_guest_signin_hint": "per accedere alle funzionalità avanzate",
|
||||
"home_processing": "Elaborazione...",
|
||||
"home_submit_button_qr": "Genera Codice QR",
|
||||
"home_submit_button": "Crea Link"
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
vBR0K1t5zNgjHxICus
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"sourceLanguageTag": "en",
|
||||
"languageTags": ["en", "de", "es", "fr", "it"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-json@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.json": {
|
||||
"pathPattern": "./messages/{languageTag}.json"
|
||||
}
|
||||
}
|
||||
7
uload/apps/web/src/app.d.ts
vendored
7
uload/apps/web/src/app.d.ts
vendored
|
|
@ -1,8 +1,9 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
import type { DB } from '$lib/db';
|
||||
import type { AvailableLanguageTag } from '$paraglide/runtime';
|
||||
import type { ParaglideLocals } from '@inlang/paraglide-sveltekit';
|
||||
|
||||
// Supported locales
|
||||
export type SupportedLocale = 'en' | 'de' | 'es' | 'fr' | 'it';
|
||||
|
||||
// User type (will be replaced by external auth later)
|
||||
export interface User {
|
||||
|
|
@ -20,7 +21,7 @@ declare global {
|
|||
interface Locals {
|
||||
db: DB;
|
||||
user: User | null;
|
||||
paraglide: ParaglideLocals<AvailableLanguageTag>;
|
||||
locale: SupportedLocale;
|
||||
}
|
||||
interface PageData {
|
||||
user: User | null;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
getFreeText,
|
||||
type VariantContent
|
||||
} from '../config/variants';
|
||||
import { getLocale } from '$paraglide/runtime.js';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PageData, ActionData } from '../../../routes/$types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -42,14 +43,14 @@
|
|||
// Log for debugging
|
||||
if (showDebug) {
|
||||
console.log('A/B Test Variant:', variant, content);
|
||||
console.log('Current Locale:', getLocale());
|
||||
console.log('Current Locale:', get(locale));
|
||||
}
|
||||
});
|
||||
|
||||
// React to locale changes - use derived state
|
||||
$effect(() => {
|
||||
// This will re-run when locale changes
|
||||
const currentLocale = getLocale();
|
||||
const currentLocale = get(locale);
|
||||
|
||||
// Update content based on current locale
|
||||
content = getVariantContent(variant);
|
||||
|
|
@ -82,7 +83,7 @@
|
|||
<div class="font-bold text-green-400">A/B Test Debug</div>
|
||||
<div>Variant: <span class="text-yellow-400">{variant}</span></div>
|
||||
<div>Name: {content.name}</div>
|
||||
<div>Locale: <span class="text-blue-400">{getLocale()}</span></div>
|
||||
<div>Locale: <span class="text-blue-400">{get(locale)}</span></div>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { setLocale, getLocale } from '$paraglide/runtime.js';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import '$lib/i18n';
|
||||
|
||||
let showDropdown = $state(false);
|
||||
|
||||
|
|
@ -14,7 +16,7 @@
|
|||
// Get current language on mount
|
||||
$effect(() => {
|
||||
if (browser) {
|
||||
const currentCode = getLocale();
|
||||
const currentCode = get(locale) || 'en';
|
||||
currentLanguage = languages.find((lang) => lang.code === currentCode) || languages[0];
|
||||
}
|
||||
});
|
||||
|
|
@ -23,8 +25,8 @@
|
|||
if (browser) {
|
||||
// Save preference
|
||||
localStorage.setItem('preferred-language', langCode);
|
||||
// Update Paraglide locale
|
||||
setLocale(langCode as any);
|
||||
// Update svelte-i18n locale
|
||||
locale.set(langCode);
|
||||
// Update current language display
|
||||
currentLanguage = languages.find((lang) => lang.code === langCode) || languages[0];
|
||||
// Close dropdown
|
||||
|
|
|
|||
15
uload/apps/web/src/routes/(auth)/+layout.svelte
Normal file
15
uload/apps/web/src/routes/(auth)/+layout.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { data, children }: { data: any; children: Snippet } = $props();
|
||||
|
||||
$effect(() => {
|
||||
// Redirect to dashboard if already logged in
|
||||
if (data.user) {
|
||||
goto('/my');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { UloadLogo } from '@manacore/shared-branding';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
|
||||
async function handleForgotPassword(email: string) {
|
||||
try {
|
||||
await pb.collection('users').requestPasswordReset(email);
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
// PocketBase doesn't reveal if email exists for security
|
||||
// So we always show success message
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ForgotPasswordPage
|
||||
appName="uLoad"
|
||||
logo={UloadLogo}
|
||||
primaryColor="#3b82f6"
|
||||
onForgotPassword={handleForgotPassword}
|
||||
goto={goto}
|
||||
loginPath="/login"
|
||||
lightBackground="#f8fafc"
|
||||
darkBackground="#0f172a"
|
||||
translations={{
|
||||
titleForm: 'Passwort zurücksetzen',
|
||||
titleSuccess: 'E-Mail gesendet',
|
||||
description: 'Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen deines Passworts.',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
sendResetLinkButton: 'Link senden',
|
||||
sending: 'Wird gesendet...',
|
||||
backToLogin: 'Zurück zum Login',
|
||||
resendEmail: 'E-Mail erneut senden',
|
||||
successMessage: 'Wir haben einen Link zum Zurücksetzen deines Passworts an {email} gesendet. Bitte überprüfe deinen Posteingang.',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
sendFailed: 'Senden der E-Mail fehlgeschlagen'
|
||||
}}
|
||||
/>
|
||||
62
uload/apps/web/src/routes/(auth)/login/+page.svelte
Normal file
62
uload/apps/web/src/routes/(auth)/login/+page.svelte
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { UloadLogo } from '@manacore/shared-branding';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
try {
|
||||
await pb.collection('users').authWithPassword(email, password);
|
||||
// Invalidate all data to refresh server-side auth state
|
||||
await invalidateAll();
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: err?.message || 'Ungültige E-Mail oder Passwort'
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginPage
|
||||
appName="uLoad"
|
||||
logo={UloadLogo}
|
||||
primaryColor="#3b82f6"
|
||||
onSignIn={handleSignIn}
|
||||
goto={goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/my"
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#f8fafc"
|
||||
darkBackground="#0f172a"
|
||||
translations={{
|
||||
title: 'Anmelden',
|
||||
subtitle: 'Melde dich mit deinem uLoad Account an',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
passwordPlaceholder: 'Passwort',
|
||||
rememberMe: 'Angemeldet bleiben',
|
||||
forgotPassword: 'Passwort vergessen?',
|
||||
signInButton: 'Anmelden',
|
||||
signingIn: 'Wird angemeldet...',
|
||||
success: 'Erfolg!',
|
||||
orDivider: 'oder',
|
||||
noAccount: 'Noch kein Account?',
|
||||
createAccount: 'Jetzt registrieren',
|
||||
skipToForm: 'Zum Login-Formular springen',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
|
||||
passwordRequired: 'Passwort ist erforderlich',
|
||||
signInFailed: 'Anmeldung fehlgeschlagen',
|
||||
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
|
||||
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
|
||||
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...'
|
||||
}}
|
||||
/>
|
||||
87
uload/apps/web/src/routes/(auth)/register/+page.svelte
Normal file
87
uload/apps/web/src/routes/(auth)/register/+page.svelte
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="ts">
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { UloadLogo } from '@manacore/shared-branding';
|
||||
import { pb } from '$lib/pocketbase';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
try {
|
||||
// Create user
|
||||
await pb.collection('users').create({
|
||||
email: email.toLowerCase().trim(),
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true
|
||||
});
|
||||
|
||||
// Request verification email
|
||||
try {
|
||||
await pb.collection('users').requestVerification(email);
|
||||
} catch (emailErr) {
|
||||
console.error('Failed to send verification email:', emailErr);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
needsVerification: true
|
||||
};
|
||||
} catch (err: any) {
|
||||
const errorData = err?.response?.data || err?.data || {};
|
||||
|
||||
if (errorData.email?.message?.includes('unique')) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Diese E-Mail ist bereits registriert. Bitte melde dich an.'
|
||||
};
|
||||
}
|
||||
|
||||
if (errorData.email?.message) {
|
||||
return { success: false, error: errorData.email.message };
|
||||
}
|
||||
|
||||
if (errorData.password?.message) {
|
||||
return { success: false, error: errorData.password.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: err?.message || 'Registrierung fehlgeschlagen. Bitte versuche es erneut.'
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<RegisterPage
|
||||
appName="uLoad"
|
||||
logo={UloadLogo}
|
||||
primaryColor="#3b82f6"
|
||||
onSignUp={handleSignUp}
|
||||
goto={goto}
|
||||
successRedirect="/login?registered=true"
|
||||
loginPath="/login"
|
||||
lightBackground="#f8fafc"
|
||||
darkBackground="#0f172a"
|
||||
translations={{
|
||||
title: 'Account erstellen',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
passwordPlaceholder: 'Passwort',
|
||||
confirmPasswordPlaceholder: 'Passwort bestätigen',
|
||||
passwordRequirements: 'Passwort muss mindestens 8 Zeichen mit Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten.',
|
||||
createAccountButton: 'Account erstellen',
|
||||
creatingAccount: 'Wird erstellt...',
|
||||
backToLogin: 'Zurück zum Login',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
passwordRequired: 'Passwort ist erforderlich',
|
||||
confirmPasswordRequired: 'Bitte bestätige dein Passwort',
|
||||
passwordsDoNotMatch: 'Passwörter stimmen nicht überein',
|
||||
passwordTooShort: 'Passwort muss mindestens 8 Zeichen haben',
|
||||
passwordStrengthError: 'Passwort muss Kleinbuchstaben, Großbuchstaben, Zahl und Sonderzeichen enthalten',
|
||||
registrationFailed: 'Registrierung fehlgeschlagen',
|
||||
accountCreated: 'Account erstellt! Bitte überprüfe deine E-Mail zur Verifizierung.'
|
||||
}}
|
||||
/>
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: '🔗',
|
||||
title: 'URL-Verkürzung',
|
||||
description: 'Verwandeln Sie lange URLs in kurze, teilbare Links mit nur einem Klick.'
|
||||
},
|
||||
{
|
||||
icon: '📊',
|
||||
title: 'Detaillierte Analytics',
|
||||
description: 'Verfolgen Sie Klicks, Herkunft und Engagement Ihrer Links in Echtzeit.'
|
||||
},
|
||||
{
|
||||
icon: '💳',
|
||||
title: 'Digitale Visitenkarten',
|
||||
description: 'Erstellen Sie professionelle digitale Visitenkarten mit QR-Codes.'
|
||||
},
|
||||
{
|
||||
icon: '🎨',
|
||||
title: 'Anpassbare Templates',
|
||||
description: 'Nutzen Sie vorgefertigte Templates oder erstellen Sie eigene Designs.'
|
||||
},
|
||||
{
|
||||
icon: '🔒',
|
||||
title: 'Passwortschutz',
|
||||
description: 'Schützen Sie Ihre Links mit Passwörtern und Ablaufdaten.'
|
||||
},
|
||||
{
|
||||
icon: '🏷️',
|
||||
title: 'Tag-System',
|
||||
description: 'Organisieren Sie Ihre Links mit Tags für bessere Übersicht.'
|
||||
}
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ value: '10K+', label: 'Aktive Nutzer' },
|
||||
{ value: '500K+', label: 'Erstellte Links' },
|
||||
{ value: '2M+', label: 'Klicks verfolgt' },
|
||||
{ value: '99.9%', label: 'Uptime' }
|
||||
];
|
||||
|
||||
const team = [
|
||||
{
|
||||
name: 'Till Schneider',
|
||||
role: 'Gründer & Entwickler',
|
||||
description: 'Full-Stack Entwickler mit Leidenschaft für saubere, effiziente Lösungen.',
|
||||
avatar: '👨💻'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.title || 'Über Uload'}</title>
|
||||
<meta name="description" content={data.description || 'Erfahren Sie mehr über Uload'} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<Navigation user={data.user} />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative overflow-hidden border-b border-theme-border bg-theme-surface">
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
||||
<div class="relative mx-auto max-w-7xl px-4 py-24 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold tracking-tight text-theme-text sm:text-5xl md:text-6xl">
|
||||
Über <span class="text-theme-primary">Uload</span>
|
||||
</h1>
|
||||
<p class="mx-auto mt-6 max-w-2xl text-lg text-theme-text-muted">
|
||||
Ihre moderne Plattform für professionelles Link-Management und digitale Präsenz
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mission Section -->
|
||||
<section class="bg-theme-background py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<h2 class="text-3xl font-bold text-theme-text">Unsere Mission</h2>
|
||||
<p class="mt-6 text-lg leading-relaxed text-theme-text-muted">
|
||||
Bei Uload glauben wir daran, dass Link-Management einfach, effizient und zugänglich sein sollte.
|
||||
Unsere Plattform wurde entwickelt, um Unternehmen und Einzelpersonen dabei zu helfen,
|
||||
ihre Online-Präsenz zu optimieren und wertvolle Einblicke in ihr Publikum zu gewinnen.
|
||||
</p>
|
||||
<p class="mt-4 text-lg leading-relaxed text-theme-text-muted">
|
||||
Von der einfachen URL-Verkürzung bis hin zu erweiterten Analytics und digitalen Visitenkarten -
|
||||
wir bieten alle Tools, die Sie für erfolgreiches digitales Marketing benötigen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<section class="bg-theme-surface py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-theme-text">Was macht uns besonders?</h2>
|
||||
<p class="mt-4 text-lg text-theme-text-muted">
|
||||
Entdecken Sie die Features, die Uload zur ersten Wahl für Link-Management machen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each features as feature}
|
||||
<div class="group rounded-lg border border-theme-border bg-theme-surface-hover p-6 transition-all hover:shadow-md hover:scale-105">
|
||||
<div class="mb-4 text-4xl">{feature.icon}</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-theme-text">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="bg-theme-primary py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-white">Uload in Zahlen</h2>
|
||||
<p class="mt-4 text-lg text-gray-200">
|
||||
Vertrauen Sie auf eine bewährte Plattform
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid grid-cols-2 gap-8 md:grid-cols-4">
|
||||
{#each stats as stat}
|
||||
<div class="text-center">
|
||||
<div class="text-4xl font-bold text-white">{stat.value}</div>
|
||||
<div class="mt-2 text-sm text-gray-200">{stat.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Team Section -->
|
||||
<section class="bg-theme-background py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-theme-text">Das Team</h2>
|
||||
<p class="mt-4 text-lg text-theme-text-muted">
|
||||
Die Menschen hinter Uload
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
{#each team as member}
|
||||
<div class="max-w-sm rounded-lg border border-theme-border bg-theme-surface p-8 text-center shadow-lg">
|
||||
<div class="mx-auto mb-4 flex h-24 w-24 items-center justify-center rounded-full bg-theme-surface-hover text-5xl">
|
||||
{member.avatar}
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-theme-text">{member.name}</h3>
|
||||
<p class="mt-1 text-sm text-theme-accent">{member.role}</p>
|
||||
<p class="mt-4 text-theme-text-muted">{member.description}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Values Section -->
|
||||
<section class="bg-theme-surface py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-theme-text">Unsere Werte</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid gap-8 md:grid-cols-3">
|
||||
<div class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-theme-surface-hover border border-theme-border text-2xl">
|
||||
🚀
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-theme-text">Innovation</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Wir entwickeln ständig neue Features und verbessern bestehende Funktionen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-theme-surface-hover border border-theme-border text-2xl">
|
||||
🛡️
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-theme-text">Sicherheit</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Ihre Daten sind bei uns sicher. Datenschutz und Sicherheit haben höchste Priorität.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-theme-surface-hover border border-theme-border text-2xl">
|
||||
💡
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-semibold text-theme-text">Einfachheit</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Komplexe Funktionen, einfach zu bedienen. Das ist unser Versprechen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="bg-theme-background py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="rounded-2xl bg-theme-primary px-8 py-12 text-center shadow-xl">
|
||||
<h2 class="text-3xl font-bold text-white">Bereit loszulegen?</h2>
|
||||
<p class="mt-4 text-lg text-gray-200">
|
||||
Erstellen Sie noch heute Ihr kostenloses Konto und entdecken Sie die Möglichkeiten
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<a
|
||||
href="/register"
|
||||
class="rounded-lg bg-white px-8 py-3 font-semibold text-theme-primary transition hover:bg-gray-50 hover:scale-105"
|
||||
>
|
||||
Kostenlos starten
|
||||
</a>
|
||||
<a
|
||||
href="/features"
|
||||
class="rounded-lg border-2 border-white px-8 py-3 font-semibold text-white transition hover:bg-white/10 hover:scale-105"
|
||||
>
|
||||
Features entdecken
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section class="bg-theme-surface py-16">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
<h2 class="text-3xl font-bold text-theme-text">Kontakt</h2>
|
||||
<p class="mt-4 text-lg text-theme-text-muted">
|
||||
Haben Sie Fragen oder Feedback? Wir freuen uns von Ihnen zu hören!
|
||||
</p>
|
||||
<div class="mt-8 flex flex-col items-center gap-4">
|
||||
<a
|
||||
href="mailto:support@ulo.ad"
|
||||
class="flex items-center gap-2 text-theme-accent hover:text-theme-accent-hover transition-colors"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
support@ulo.ad
|
||||
</a>
|
||||
<a
|
||||
href="/features"
|
||||
class="flex items-center gap-2 text-theme-accent hover:text-theme-accent-hover transition-colors"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
Feature-Wünsche einreichen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-grid-pattern {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%239C92AC' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
return {
|
||||
title: 'Über Uload - Moderne URL-Verkürzung & Link-Management',
|
||||
description: 'Erfahren Sie mehr über Uload - Ihre Plattform für professionelles Link-Management, URL-Verkürzung und digitale Visitenkarten.'
|
||||
};
|
||||
};
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<Navigation user={data.user} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<h1 class="mb-8 text-3xl font-bold text-theme-text">Allgemeine Geschäftsbedingungen (AGB)</h1>
|
||||
|
||||
<div class="space-y-6 text-theme-text">
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 1 Geltungsbereich</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für alle über
|
||||
unsere Website uload geschlossenen Verträge zwischen uns und unseren Nutzern
|
||||
(nachfolgend "Nutzer" oder "Sie").
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Maßgeblich ist die jeweils zum Zeitpunkt des Vertragsschlusses gültige Fassung der
|
||||
AGB.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Abweichende Bedingungen des Nutzers werden nicht anerkannt, es sei denn, wir stimmen
|
||||
ihrer Geltung ausdrücklich schriftlich zu.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 2 Vertragsgegenstand</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Gegenstand des Vertrages ist die Bereitstellung eines URL-Verkürzungsdienstes
|
||||
(uload) sowie damit verbundene Zusatzfunktionen.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Der Nutzer kann über unseren Dienst lange URLs in kurze, leicht zu teilende Links
|
||||
umwandeln.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Je nach gewähltem Tarif stehen dem Nutzer verschiedene Funktionen zur Verfügung, wie
|
||||
z.B. Statistiken, benutzerdefinierte Links, QR-Codes und erweiterte
|
||||
Verwaltungsfunktionen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 3 Registrierung und Nutzerkonto</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Für die vollständige Nutzung unseres Dienstes ist eine Registrierung erforderlich.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Bei der Registrierung müssen alle Pflichtfelder wahrheitsgemäß und vollständig
|
||||
ausgefüllt werden.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Der Nutzer ist verpflichtet, seine Zugangsdaten geheim zu halten und vor dem Zugriff
|
||||
Dritter zu schützen.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(4) Der Nutzer haftet für alle unter seinem Nutzerkonto vorgenommenen Handlungen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 4 Leistungen und Verfügbarkeit</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Wir stellen unseren Dienst mit einer Verfügbarkeit von 99% im Jahresmittel zur
|
||||
Verfügung.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Hiervon ausgenommen sind Zeiten, in denen der Server aufgrund von technischen oder
|
||||
sonstigen Problemen, die nicht in unserem Einflussbereich liegen (höhere Gewalt,
|
||||
Verschulden Dritter etc.), nicht erreichbar ist.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Wir behalten uns vor, den Zugang zum Dienst bei Wartungsarbeiten vorübergehend zu
|
||||
beschränken.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 5 Preise und Zahlung</h2>
|
||||
<p class="text-theme-text-muted">(1) Die Nutzung des Basisdienstes ist kostenlos.</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Für erweiterte Funktionen (Premium-Tarife) fallen die auf unserer Website
|
||||
angegebenen Gebühren an.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Die Abrechnung erfolgt wahlweise monatlich oder jährlich im Voraus.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(4) Die Zahlung erfolgt über die auf der Website angebotenen Zahlungsmethoden (z.B.
|
||||
Kreditkarte, PayPal, Stripe).
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(5) Bei Zahlungsverzug behalten wir uns vor, den Zugang zu Premium-Funktionen zu
|
||||
sperren.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 6 Pflichten des Nutzers</h2>
|
||||
<p class="mb-2 text-theme-text-muted">Der Nutzer verpflichtet sich:</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>
|
||||
keine rechtswidrigen, beleidigenden, verleumderischen oder anderweitig unzulässigen
|
||||
Inhalte zu verlinken
|
||||
</li>
|
||||
<li>keine Links zu erstellen, die gegen geltendes Recht verstoßen</li>
|
||||
<li>keine automatisierten Anfragen ohne unsere ausdrückliche Genehmigung zu senden</li>
|
||||
<li>den Dienst nicht für Spam oder Phishing zu verwenden</li>
|
||||
<li>keine schädliche Software oder Malware zu verbreiten</li>
|
||||
<li>
|
||||
die Rechte Dritter, insbesondere Marken-, Urheber- und Persönlichkeitsrechte zu
|
||||
beachten
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 7 Haftung</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Wir haften unbeschränkt für Vorsatz und grobe Fahrlässigkeit.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Bei leichter Fahrlässigkeit haften wir nur bei Verletzung einer wesentlichen
|
||||
Vertragspflicht (Kardinalpflicht) und nur in Höhe des vorhersehbaren, typischerweise
|
||||
eintretenden Schadens.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Die Haftung für Datenverlust wird auf den typischen Wiederherstellungsaufwand
|
||||
beschränkt, der bei regelmäßiger und gefahrentsprechender Anfertigung von
|
||||
Sicherungskopien eingetreten wäre.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(4) Die vorstehenden Haftungsbeschränkungen gelten nicht bei Verletzung von Leben,
|
||||
Körper und Gesundheit sowie bei Ansprüchen nach dem Produkthaftungsgesetz.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 8 Laufzeit und Kündigung</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Der Vertrag über die kostenlose Nutzung wird auf unbestimmte Zeit geschlossen und
|
||||
kann von beiden Parteien jederzeit ohne Einhaltung einer Frist gekündigt werden.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Premium-Tarife haben eine Mindestlaufzeit entsprechend des gewählten
|
||||
Abrechnungszeitraums (monatlich oder jährlich).
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Premium-Tarife verlängern sich automatisch um den gewählten Abrechnungszeitraum,
|
||||
wenn sie nicht mit einer Frist von 14 Tagen zum Ende der Laufzeit gekündigt werden.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(4) Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(5) Kündigungen bedürfen der Textform (z.B. E-Mail).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 9 Datenschutz</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Wir erheben, verarbeiten und nutzen personenbezogene Daten nur im Rahmen der
|
||||
gesetzlichen Bestimmungen.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Einzelheiten zur Datenverarbeitung sind unserer Datenschutzerklärung zu entnehmen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 10 Änderungen der AGB</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Wir behalten uns vor, diese AGB jederzeit zu ändern.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Änderungen werden dem Nutzer per E-Mail mitgeteilt.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Die Änderungen gelten als genehmigt, wenn der Nutzer nicht innerhalb von vier Wochen
|
||||
nach Mitteilung der Änderungen widerspricht.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(4) Bei Widerspruch steht beiden Parteien ein Sonderkündigungsrecht zu.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 11 Streitbeilegung</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS)
|
||||
bereit, die Sie unter
|
||||
<a
|
||||
href="https://ec.europa.eu/consumers/odr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-theme-primary hover:underline">https://ec.europa.eu/consumers/odr/</a
|
||||
> finden.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">§ 12 Schlussbestimmungen</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
(1) Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(2) Ist der Nutzer Kaufmann, juristische Person des öffentlichen Rechts oder
|
||||
öffentlich-rechtliches Sondervermögen, ist ausschließlicher Gerichtsstand für alle
|
||||
Streitigkeiten aus diesem Vertrag unser Geschäftssitz.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
(3) Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, bleibt die
|
||||
Wirksamkeit der übrigen Bestimmungen unberührt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="pt-4">
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Stand: {new Date().toLocaleDateString('de-DE', { year: 'numeric', month: 'long' })}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 rounded-lg bg-yellow-50 p-4 text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"
|
||||
>
|
||||
<p class="font-semibold">⚠️ Wichtiger Hinweis:</p>
|
||||
<p>
|
||||
Diese AGB sind ein Muster und sollten von einem Rechtsanwalt auf Ihre spezifischen
|
||||
Bedürfnisse angepasst werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
return {
|
||||
user
|
||||
};
|
||||
};
|
||||
10
uload/apps/web/src/routes/api/health/+server.ts
Normal file
10
uload/apps/web/src/routes/api/health/+server.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { getCollection, getFeaturedPosts, getAllCategories } from '$lib/content';
|
||||
import type { BlogPostWithMeta, BlogCategory } from '../../content/config';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const posts = await getCollection<BlogPostWithMeta>('blog');
|
||||
const featuredPosts = await getFeaturedPosts();
|
||||
const categories = await getAllCategories();
|
||||
|
||||
return {
|
||||
posts,
|
||||
featuredPosts,
|
||||
categories,
|
||||
user: locals.user
|
||||
};
|
||||
};
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { BlogPostWithMeta } from '../../content/config';
|
||||
import BlogCard from '$lib/components/blog/BlogCard.svelte';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import ViewToggle from '$lib/components/ViewToggle.svelte';
|
||||
import { Search, ArrowUpDown } from 'lucide-svelte';
|
||||
|
||||
// Svelte 5: Props mit $props()
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Svelte 5: $state für alle reaktiven Variablen
|
||||
let selectedCategory = $state<string>('all');
|
||||
let selectedTag = $state<string | null>(null);
|
||||
let searchQuery = $state('');
|
||||
let sortBy = $state<'date' | 'readingTime' | 'category'>('date');
|
||||
let sortOrder = $state<'asc' | 'desc'>('desc');
|
||||
let viewMode = $state<'cards' | 'list' | 'stats'>('cards');
|
||||
|
||||
// Svelte 5: $derived für gefilterte/sortierte Posts
|
||||
let filteredAndSortedPosts = $derived.by(() => {
|
||||
let posts = [...data.posts];
|
||||
|
||||
// Kategorie-Filter
|
||||
if (selectedCategory !== 'all') {
|
||||
posts = posts.filter(p => p.category === selectedCategory);
|
||||
}
|
||||
|
||||
// Tag-Filter
|
||||
if (selectedTag) {
|
||||
posts = posts.filter(p => p.tags.includes(selectedTag));
|
||||
}
|
||||
|
||||
// Suche
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
posts = posts.filter(p =>
|
||||
p.title.toLowerCase().includes(query) ||
|
||||
p.excerpt.toLowerCase().includes(query) ||
|
||||
p.tags.some(t => t.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
posts.sort((a, b) => {
|
||||
let compareValue = 0;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'date':
|
||||
compareValue = new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||
break;
|
||||
case 'readingTime':
|
||||
compareValue = b.readingTime - a.readingTime;
|
||||
break;
|
||||
case 'category':
|
||||
compareValue = a.category.localeCompare(b.category);
|
||||
break;
|
||||
}
|
||||
|
||||
return sortOrder === 'asc' ? -compareValue : compareValue;
|
||||
});
|
||||
|
||||
return posts;
|
||||
});
|
||||
|
||||
// Svelte 5: $derived für Tag-Cloud mit Counts
|
||||
let tagCloud = $derived(() => {
|
||||
const tags = new Map<string, number>();
|
||||
data.posts.forEach(post => {
|
||||
post.tags.forEach(tag => {
|
||||
tags.set(tag, (tags.get(tag) || 0) + 1);
|
||||
});
|
||||
});
|
||||
return Array.from(tags.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 20);
|
||||
});
|
||||
|
||||
// Svelte 5: $derived für Statistiken
|
||||
let stats = $derived({
|
||||
totalPosts: filteredAndSortedPosts.length,
|
||||
totalCategories: data.categories.length,
|
||||
totalTags: tagCloud.length
|
||||
});
|
||||
|
||||
// Event Handler
|
||||
function handleCategorySelect(category: string) {
|
||||
selectedCategory = category;
|
||||
selectedTag = null;
|
||||
}
|
||||
|
||||
function handleTagSelect(tag: string) {
|
||||
selectedTag = selectedTag === tag ? null : tag;
|
||||
selectedCategory = 'all';
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
selectedCategory = 'all';
|
||||
selectedTag = null;
|
||||
searchQuery = '';
|
||||
sortBy = 'date';
|
||||
sortOrder = 'desc';
|
||||
}
|
||||
|
||||
function toggleSortOrder() {
|
||||
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blog | uload - Insights über URLs, Marketing und Psychologie</title>
|
||||
<meta name="description" content="Entdecken Sie Artikel über URL-Psychologie, Marketing-Strategien und Best Practices für Link-Management." />
|
||||
</svelte:head>
|
||||
|
||||
<Navigation user={data.user} currentPath="/blog" />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- Header mit Titel und View Toggle -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold text-theme-text">Blog</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<ViewToggle
|
||||
currentView={viewMode}
|
||||
onViewChange={(view) => viewMode = view}
|
||||
showStats={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Sort Controls -->
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Artikel durchsuchen..."
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface pl-10 pr-4 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="sort-select" class="text-sm font-medium text-theme-text">Sortieren nach:</label>
|
||||
<select
|
||||
id="sort-select"
|
||||
bind:value={sortBy}
|
||||
class="rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-sm text-theme-text focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
>
|
||||
<option value="date">Datum</option>
|
||||
<option value="readingTime">Lesezeit</option>
|
||||
<option value="category">Kategorie</option>
|
||||
</select>
|
||||
<button
|
||||
onclick={toggleSortOrder}
|
||||
class="rounded-lg border border-theme-border bg-theme-surface p-2 text-theme-text transition-all hover:bg-theme-surface-hover"
|
||||
aria-label="Toggle sort order"
|
||||
>
|
||||
<ArrowUpDown class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="mb-6 rounded-xl border border-theme-border bg-theme-surface p-6">
|
||||
<!-- Kategorien -->
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-3 text-sm font-semibold text-theme-text">Kategorien</h3>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onclick={() => handleCategorySelect('all')}
|
||||
class="rounded-lg border px-3 py-1.5 text-sm font-medium transition-all {
|
||||
selectedCategory === 'all'
|
||||
? 'border-theme-primary bg-theme-primary text-white'
|
||||
: 'border-theme-border bg-theme-surface text-theme-text hover:bg-theme-surface-hover'
|
||||
}"
|
||||
>
|
||||
Alle ({data.posts.length})
|
||||
</button>
|
||||
{#each data.categories as category}
|
||||
<button
|
||||
onclick={() => handleCategorySelect(category.slug)}
|
||||
class="rounded-lg border px-3 py-1.5 text-sm font-medium transition-all {
|
||||
selectedCategory === category.slug
|
||||
? 'border-theme-primary bg-theme-primary text-white'
|
||||
: 'border-theme-border bg-theme-surface text-theme-text hover:bg-theme-surface-hover'
|
||||
}"
|
||||
>
|
||||
{category.name} ({category.count})
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tag-Cloud -->
|
||||
{#if tagCloud.length > 0}
|
||||
<div>
|
||||
<h3 class="mb-3 text-sm font-semibold text-theme-text">Beliebte Tags</h3>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#each tagCloud as [tag, count]}
|
||||
<button
|
||||
onclick={() => handleTagSelect(tag)}
|
||||
class="rounded-full border px-3 py-1 text-sm transition-all {
|
||||
selectedTag === tag
|
||||
? 'border-theme-primary bg-theme-primary/10 text-theme-primary'
|
||||
: 'border-theme-border bg-theme-surface text-theme-text-muted hover:bg-theme-surface-hover'
|
||||
}"
|
||||
>
|
||||
#{tag} ({count})
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Active Filters -->
|
||||
{#if selectedCategory !== 'all' || selectedTag || searchQuery}
|
||||
<div class="mt-4 flex items-center gap-2 border-t border-theme-border pt-4">
|
||||
<span class="text-sm font-medium text-theme-text-muted">Aktive Filter:</span>
|
||||
{#if selectedCategory !== 'all'}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-theme-primary/10 px-2 py-1 text-xs font-medium text-theme-primary">
|
||||
{data.categories.find(c => c.slug === selectedCategory)?.name || selectedCategory}
|
||||
<button
|
||||
onclick={() => selectedCategory = 'all'}
|
||||
class="ml-1 hover:text-theme-primary-hover"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
{#if selectedTag}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-theme-primary/10 px-2 py-1 text-xs font-medium text-theme-primary">
|
||||
#{selectedTag}
|
||||
<button
|
||||
onclick={() => selectedTag = null}
|
||||
class="ml-1 hover:text-theme-primary-hover"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
{#if searchQuery}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-theme-primary/10 px-2 py-1 text-xs font-medium text-theme-primary">
|
||||
"{searchQuery}"
|
||||
<button
|
||||
onclick={() => searchQuery = ''}
|
||||
class="ml-1 hover:text-theme-primary-hover"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
<button
|
||||
onclick={clearFilters}
|
||||
class="ml-auto text-sm text-theme-text-muted hover:text-theme-text"
|
||||
>
|
||||
Alle löschen
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Featured Posts -->
|
||||
{#if data.featuredPosts.length > 0 && selectedCategory === 'all' && !selectedTag && !searchQuery}
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-theme-text">Featured Artikel</h2>
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{#each data.featuredPosts as post}
|
||||
<BlogCard {post} featured={true} {viewMode} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Posts Grid/List -->
|
||||
{#if filteredAndSortedPosts.length > 0}
|
||||
<div class={viewMode === 'cards'
|
||||
? 'grid gap-6 sm:grid-cols-2 lg:grid-cols-3'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{#each filteredAndSortedPosts as post (post.slug)}
|
||||
<BlogCard {post} {viewMode} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-theme-text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 21a9 9 0 110-18 9 9 0 010 18z" />
|
||||
</svg>
|
||||
<p class="mt-4 text-theme-text-muted">
|
||||
Keine Artikel gefunden.
|
||||
</p>
|
||||
{#if selectedCategory !== 'all' || selectedTag || searchQuery}
|
||||
<button
|
||||
onclick={clearFilters}
|
||||
class="mt-4 text-theme-primary hover:text-theme-primary-hover"
|
||||
>
|
||||
Filter zurücksetzen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:component this={data.content} {...data.metadata} />
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const post = await import(`../../../content/blog/${params.slug}.md`);
|
||||
|
||||
return {
|
||||
content: post.default,
|
||||
metadata: post.metadata
|
||||
};
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { getCollection } from '$lib/content';
|
||||
import type { BlogPostWithMeta } from '../../../content/config';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const posts = await getCollection<BlogPostWithMeta>('blog');
|
||||
const site = 'https://ulo.ad';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>uload Blog</title>
|
||||
<link>${site}/blog</link>
|
||||
<description>Insights über URLs, Marketing und Psychologie</description>
|
||||
<language>de-DE</language>
|
||||
<atom:link href="${site}/blog/rss.xml" rel="self" type="application/rss+xml" />
|
||||
${posts.slice(0, 20).map(post => `
|
||||
<item>
|
||||
<title><![CDATA[${post.title}]]></title>
|
||||
<link>${site}/blog/${post.slug}</link>
|
||||
<guid isPermaLink="true">${site}/blog/${post.slug}</guid>
|
||||
<description><![CDATA[${post.excerpt}]]></description>
|
||||
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
|
||||
<category>${post.category}</category>
|
||||
${post.tags.map(tag => `<category>${tag}</category>`).join('\n\t\t\t')}
|
||||
</item>`).join('')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
return new Response(xml.trim(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
'Cache-Control': 'max-age=0, s-maxage=3600'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const prerender = false;
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<Navigation user={data.user} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<h1 class="mb-8 text-3xl font-bold text-theme-text">Datenschutzerklärung</h1>
|
||||
|
||||
<div class="space-y-6 text-theme-text">
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">1. Datenschutz auf einen Blick</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Allgemeine Hinweise</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren
|
||||
personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene
|
||||
Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Datenerfassung auf dieser Website</h3>
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
<strong>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</strong>
|
||||
</p>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen
|
||||
Kontaktdaten können Sie dem Impressum dieser Website entnehmen.
|
||||
</p>
|
||||
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
<strong>Wie erfassen wir Ihre Daten?</strong>
|
||||
</p>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann
|
||||
es sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben. Andere Daten
|
||||
werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere
|
||||
IT-Systeme erfasst.
|
||||
</p>
|
||||
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
<strong>Wofür nutzen wir Ihre Daten?</strong>
|
||||
</p>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu
|
||||
gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.
|
||||
</p>
|
||||
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
<strong>Welche Rechte haben Sie bezüglich Ihrer Daten?</strong>
|
||||
</p>
|
||||
<p class="text-theme-text-muted">
|
||||
Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck
|
||||
Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht,
|
||||
die Berichtigung oder Löschung dieser Daten zu verlangen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">2. Hosting</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
Wir hosten die Inhalte unserer Website bei folgendem Anbieter: [Hosting-Anbieter
|
||||
einfügen]
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">
|
||||
3. Allgemeine Hinweise und Pflichtinformationen
|
||||
</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Datenschutz</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir
|
||||
behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
|
||||
Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Hinweis zur verantwortlichen Stelle</h3>
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:
|
||||
</p>
|
||||
<div class="mb-4 space-y-1 text-theme-text-muted">
|
||||
<p>[Ihr Name oder Firmenname]</p>
|
||||
<p>[Ihre Adresse]</p>
|
||||
<p>Telefon: [Ihre Telefonnummer]</p>
|
||||
<p>E-Mail: [Ihre E-Mail-Adresse]</p>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Speicherdauer</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt
|
||||
wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die
|
||||
Datenverarbeitung entfällt.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">
|
||||
Gesetzlich vorgeschriebener Datenschutzbeauftragter
|
||||
</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Wir haben für unser Unternehmen einen Datenschutzbeauftragten bestellt.
|
||||
</p>
|
||||
<div class="mt-2 space-y-1 text-theme-text-muted">
|
||||
<p>[Name des Datenschutzbeauftragten]</p>
|
||||
<p>[Adresse]</p>
|
||||
<p>Telefon: [Telefonnummer]</p>
|
||||
<p>E-Mail: [E-Mail-Adresse]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">4. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Cookies</h3>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Unsere Internetseiten verwenden so genannte „Cookies". Cookies sind kleine Datenpakete
|
||||
und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für
|
||||
die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem
|
||||
Endgerät gespeichert.
|
||||
</p>
|
||||
<p class="text-theme-text-muted">
|
||||
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert
|
||||
werden und Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte
|
||||
Fälle oder generell ausschließen sowie das automatische Löschen der Cookies beim
|
||||
Schließen des Browsers aktivieren.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Server-Log-Dateien</h3>
|
||||
<p class="mb-2 text-theme-text-muted">
|
||||
Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten
|
||||
Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
||||
</p>
|
||||
<ul class="mb-4 list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>Browsertyp und Browserversion</li>
|
||||
<li>verwendetes Betriebssystem</li>
|
||||
<li>Referrer URL</li>
|
||||
<li>Hostname des zugreifenden Rechners</li>
|
||||
<li>Uhrzeit der Serveranfrage</li>
|
||||
<li>IP-Adresse</li>
|
||||
</ul>
|
||||
<p class="text-theme-text-muted">
|
||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen. Die
|
||||
Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Registrierung auf dieser Website</h3>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Sie können sich auf dieser Website registrieren, um zusätzliche Funktionen auf der Seite
|
||||
zu nutzen. Die dazu eingegebenen Daten verwenden wir nur zum Zwecke der Nutzung des
|
||||
jeweiligen Angebotes oder Dienstes, für den Sie sich registriert haben.
|
||||
</p>
|
||||
<p class="text-theme-text-muted">
|
||||
Die bei der Registrierung abgefragten Pflichtangaben müssen vollständig angegeben
|
||||
werden. Anderenfalls werden wir die Registrierung ablehnen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">5. Soziale Medien</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
Auf unseren Seiten sind Plugins der sozialen Netzwerke eingebunden. Wenn Sie eine
|
||||
unserer Seiten besuchen, die ein solches Plugin enthält, baut Ihr Browser eine direkte
|
||||
Verbindung mit den Servern der Netzwerke auf.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">6. Analyse-Tools und Werbung</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Google Analytics</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Diese Website nutzt Funktionen des Webanalysedienstes Google Analytics. Anbieter ist die
|
||||
Google Ireland Limited („Google"), Gordon House, Barrow Street, Dublin 4, Irland.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">7. Newsletter</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
Wenn Sie den auf der Website angebotenen Newsletter beziehen möchten, benötigen wir von
|
||||
Ihnen eine E-Mail-Adresse sowie Informationen, welche uns die Überprüfung gestatten,
|
||||
dass Sie der Inhaber der angegebenen E-Mail-Adresse sind und mit dem Empfang des
|
||||
Newsletters einverstanden sind.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">8. Plugins und Tools</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">YouTube mit erweitertem Datenschutz</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Diese Website bindet Videos der Website YouTube ein. Betreiber der Seiten ist die Google
|
||||
Ireland Limited („Google"), Gordon House, Barrow Street, Dublin 4, Irland.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Google Fonts</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten so genannte Web Fonts,
|
||||
die von Google bereitgestellt werden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">9. Online-Zahlungsanbieter</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Stripe</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Anbieter dieses Zahlungsdienstes ist die Stripe Payments Europe, Ltd., 1 Grand Canal
|
||||
Street Lower, Grand Canal Dock, Dublin, Irland (im Folgenden „Stripe").
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
Bei der Zahlung via Stripe werden die von Ihnen im Rahmen des Kaufvorgangs eingegebenen
|
||||
Zahlungsdaten (z.B. Name, E-Mail-Adresse, Kreditkartennummer, Bankleitzahl, evtl.
|
||||
Rechnungsadresse) von Stripe zur Zahlungsabwicklung gespeichert.
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
Die Übermittlung Ihrer Daten an Stripe erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b
|
||||
DSGVO (Vertragsabwicklung) sowie im Interesse einer möglichst effizienten
|
||||
Zahlungsabwicklung (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
Weitere Informationen entnehmen Sie der Datenschutzerklärung von Stripe unter folgendem
|
||||
Link:
|
||||
<a
|
||||
href="https://stripe.com/de/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-theme-primary hover:underline">https://stripe.com/de/privacy</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">10. Ihre Rechte</h2>
|
||||
<p class="mb-4 text-theme-text-muted">
|
||||
Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen jederzeit das Recht auf:
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>
|
||||
Auskunft über Ihre bei uns gespeicherten Daten und deren Verarbeitung (Art. 15 DSGVO)
|
||||
</li>
|
||||
<li>Berichtigung unrichtiger personenbezogener Daten (Art. 16 DSGVO)</li>
|
||||
<li>Löschung Ihrer bei uns gespeicherten Daten (Art. 17 DSGVO)</li>
|
||||
<li>Einschränkung der Datenverarbeitung (Art. 18 DSGVO)</li>
|
||||
<li>Widerspruch gegen die Verarbeitung Ihrer Daten (Art. 21 DSGVO)</li>
|
||||
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 rounded-lg bg-yellow-50 p-4 text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"
|
||||
>
|
||||
<p class="font-semibold">⚠️ Wichtiger Hinweis:</p>
|
||||
<p>
|
||||
Bitte ersetzen Sie alle Platzhalter in eckigen Klammern [ ] mit Ihren tatsächlichen Daten
|
||||
und passen Sie die Datenschutzerklärung an Ihre spezifischen Dienste und
|
||||
Datenverarbeitungen an.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
return {
|
||||
user
|
||||
};
|
||||
};
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
// Load published feature requests
|
||||
let featureRequestsList = [];
|
||||
let userVotes = [];
|
||||
|
||||
try {
|
||||
// Get all published feature requests, sorted by vote count
|
||||
console.log('Loading feature requests...');
|
||||
const response = await locals.pb.collection('featurerequests').getList(1, 50, {
|
||||
filter: 'published = true',
|
||||
sort: '-vote_count'
|
||||
});
|
||||
|
||||
featureRequestsList = response.items || [];
|
||||
console.log('Found feature requests:', response.totalItems, 'total');
|
||||
console.log('Feature requests loaded:', featureRequestsList.length);
|
||||
|
||||
if (featureRequestsList.length > 0) {
|
||||
console.log('First feature request:', featureRequestsList[0]);
|
||||
}
|
||||
|
||||
// If user is logged in, get their votes
|
||||
if (locals.user) {
|
||||
userVotes = await locals.pb.collection('featurevotes').getFullList({
|
||||
filter: `user_id = "${locals.user.id}"`,
|
||||
fields: 'feature_request_id'
|
||||
});
|
||||
console.log('User votes:', userVotes.length);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading feature requests:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
user: locals.user,
|
||||
featureRequests: featureRequestsList,
|
||||
userVotedIds: userVotes.map((v) => v.feature_request_id)
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
requestFeature: async ({ request, locals }) => {
|
||||
const formData = await request.formData();
|
||||
const message = formData.get('message')?.toString();
|
||||
const name = formData.get('name')?.toString() || '';
|
||||
const email = formData.get('email')?.toString() || '';
|
||||
|
||||
if (!message) {
|
||||
return fail(400, { error: 'Bitte gib eine Nachricht ein.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Save feature request to PocketBase
|
||||
const featureRequest = await locals.pb.collection('featurerequests').create({
|
||||
message,
|
||||
name: name || null,
|
||||
email: email || null,
|
||||
status: 'new'
|
||||
});
|
||||
|
||||
console.log('Feature request saved:', featureRequest.id);
|
||||
if (name || email) {
|
||||
console.log(`From: ${name || 'Anonymous'} ${email ? `(${email})` : ''}`);
|
||||
}
|
||||
console.log(`Message: ${message}`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving feature request:', error);
|
||||
return fail(500, {
|
||||
error: 'Es gab ein Problem beim Speichern deiner Anfrage. Bitte versuche es später erneut.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
vote: async ({ request, locals }) => {
|
||||
if (!locals.user) {
|
||||
return fail(401, { error: 'Du musst angemeldet sein, um abzustimmen.' });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const featureRequestId = formData.get('featureRequestId')?.toString();
|
||||
const action = formData.get('action')?.toString(); // 'add' or 'remove'
|
||||
|
||||
if (!featureRequestId || !action) {
|
||||
return fail(400, { error: 'Ungültige Anfrage.' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'add') {
|
||||
// Try to create vote - PocketBase Hook will handle vote_count update
|
||||
// The hook also prevents duplicate votes
|
||||
await locals.pb.collection('featurevotes').create({
|
||||
user_id: locals.user.id,
|
||||
feature_request_id: featureRequestId
|
||||
});
|
||||
console.log('Vote created successfully');
|
||||
} else if (action === 'remove') {
|
||||
// Find and delete vote - PocketBase Hook will handle vote_count update
|
||||
const existingVotes = await locals.pb.collection('featurevotes').getList(1, 1, {
|
||||
filter: `user_id = "${locals.user.id}" && feature_request_id = "${featureRequestId}"`
|
||||
});
|
||||
|
||||
if (existingVotes.items.length > 0) {
|
||||
await locals.pb.collection('featurevotes').delete(existingVotes.items[0].id);
|
||||
console.log('Vote removed successfully');
|
||||
}
|
||||
}
|
||||
|
||||
return { voteSuccess: true };
|
||||
} catch (error) {
|
||||
console.error('Error processing vote:', error);
|
||||
// Check if it's a duplicate vote error
|
||||
if (error.message?.includes('already voted')) {
|
||||
return fail(400, { error: 'Du hast bereits für dieses Feature abgestimmt.' });
|
||||
}
|
||||
return fail(500, { error: 'Es gab ein Problem beim Verarbeiten deiner Stimme.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,723 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData, ActionData } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
let isSubmitting = $state(false);
|
||||
let showSuccess = $state(false);
|
||||
let votingInProgress = $state<string | null>(null);
|
||||
let statusFilter = $state('all');
|
||||
let sortBy = $state('votes');
|
||||
|
||||
// Reactive filtering and sorting
|
||||
let filteredRequests = $derived(
|
||||
(data.featureRequests || [])
|
||||
.filter((req) => statusFilter === 'all' || req.status === statusFilter)
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'votes') return (b.vote_count || 0) - (a.vote_count || 0);
|
||||
if (sortBy === 'newest' && a.created && b.created) {
|
||||
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
|
||||
// Debug output
|
||||
$effect(() => {
|
||||
console.log('Feature requests from data:', data.featureRequests);
|
||||
console.log('Filtered requests:', filteredRequests);
|
||||
});
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
const badges = {
|
||||
new: { text: 'Neu', class: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' },
|
||||
reviewed: {
|
||||
text: 'In Prüfung',
|
||||
class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
},
|
||||
planned: {
|
||||
text: 'Geplant',
|
||||
class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
},
|
||||
in_progress: {
|
||||
text: 'In Entwicklung',
|
||||
class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 animate-pulse'
|
||||
},
|
||||
completed: {
|
||||
text: 'Fertig',
|
||||
class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
||||
},
|
||||
rejected: {
|
||||
text: 'Abgelehnt',
|
||||
class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
|
||||
}
|
||||
};
|
||||
return badges[status] || badges.new;
|
||||
}
|
||||
|
||||
function getCategoryIcon(category: string) {
|
||||
const icons = {
|
||||
ui: '🎨',
|
||||
performance: '⚡',
|
||||
integration: '🔌',
|
||||
security: '🔐',
|
||||
features: '✨',
|
||||
other: '💡'
|
||||
};
|
||||
return icons[category] || '💡';
|
||||
}
|
||||
|
||||
const features = [
|
||||
{
|
||||
category: 'Link Management',
|
||||
icon: '🔗',
|
||||
items: [
|
||||
{
|
||||
title: 'Smart URL Shortening',
|
||||
description: 'Verwandle lange URLs in kurze, einprägsame Links mit einem Klick',
|
||||
icon: '✂️'
|
||||
},
|
||||
{
|
||||
title: 'Custom Short Codes',
|
||||
description: 'Erstelle eigene, personalisierte Kurz-URLs für deine Marke',
|
||||
icon: '✏️'
|
||||
},
|
||||
{
|
||||
title: 'Ordner Organisation',
|
||||
description: 'Organisiere Links in farbcodierten Ordnern mit Icons',
|
||||
icon: '📁'
|
||||
},
|
||||
{
|
||||
title: 'Tagging System',
|
||||
description: 'Verwende Tags mit Farben und Icons für bessere Organisation',
|
||||
icon: '🏷️'
|
||||
},
|
||||
{
|
||||
title: 'QR Code Generator',
|
||||
description: 'Erstelle anpassbare QR Codes in verschiedenen Formaten und Farben',
|
||||
icon: '📱'
|
||||
},
|
||||
{
|
||||
title: 'Bulk Operations',
|
||||
description: 'Verwalte mehrere Links gleichzeitig mit Massenaktionen',
|
||||
icon: '⚡'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Sicherheit & Kontrolle',
|
||||
icon: '🔐',
|
||||
items: [
|
||||
{
|
||||
title: 'Passwortschutz',
|
||||
description: 'Sichere sensible Links mit individuellen Passwörtern',
|
||||
icon: '🔒'
|
||||
},
|
||||
{
|
||||
title: 'Ablaufdatum',
|
||||
description: 'Setze automatische Ablaufzeiten für temporäre Links',
|
||||
icon: '⏰'
|
||||
},
|
||||
{
|
||||
title: 'Click Limits',
|
||||
description: 'Limitiere die Anzahl der Aufrufe pro Link',
|
||||
icon: '🎯'
|
||||
},
|
||||
{
|
||||
title: 'Link Deaktivierung',
|
||||
description: 'Aktiviere und deaktiviere Links jederzeit nach Bedarf',
|
||||
icon: '🔄'
|
||||
},
|
||||
{
|
||||
title: 'SSL Verschlüsselung',
|
||||
description: 'Alle Daten sind mit modernster SSL-Technologie geschützt',
|
||||
icon: '🛡️'
|
||||
},
|
||||
{
|
||||
title: 'GDPR Konform',
|
||||
description: 'Vollständige DSGVO-Konformität für europäische Nutzer',
|
||||
icon: '🇪🇺'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Analytics & Insights',
|
||||
icon: '📊',
|
||||
items: [
|
||||
{
|
||||
title: 'Echtzeit-Statistiken',
|
||||
description: 'Verfolge Klicks und Aktivitäten in Echtzeit',
|
||||
icon: '📈'
|
||||
},
|
||||
{
|
||||
title: 'Geräte-Analyse',
|
||||
description: 'Erkenne Mobile, Desktop und Tablet Zugriffe',
|
||||
icon: '💻'
|
||||
},
|
||||
{
|
||||
title: 'Browser-Statistiken',
|
||||
description: 'Detaillierte Aufschlüsselung nach Browsern',
|
||||
icon: '🌐'
|
||||
},
|
||||
{
|
||||
title: 'Geografische Daten',
|
||||
description: 'Sehe woher deine Besucher kommen',
|
||||
icon: '🗺️'
|
||||
},
|
||||
{
|
||||
title: 'Referrer Tracking',
|
||||
description: 'Verstehe von welchen Seiten Traffic kommt',
|
||||
icon: '🔍'
|
||||
},
|
||||
{
|
||||
title: 'Export Funktionen',
|
||||
description: 'Exportiere Analytics-Daten für weitere Analysen',
|
||||
icon: '📥'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Personalisierung',
|
||||
icon: '🎨',
|
||||
items: [
|
||||
{
|
||||
title: 'Öffentliche Profile',
|
||||
description: 'Erstelle deine persönliche Link-Seite mit Bio und Social Links',
|
||||
icon: '👤'
|
||||
},
|
||||
{
|
||||
title: 'Custom Themes',
|
||||
description: 'Wähle aus 5 verschiedenen Themes oder nutze Dark Mode',
|
||||
icon: '🎭'
|
||||
},
|
||||
{
|
||||
title: 'Mehrsprachigkeit',
|
||||
description:
|
||||
'5 Sprachen verfügbar: Deutsch, Englisch, Französisch, Spanisch, Italienisch',
|
||||
icon: '🌍'
|
||||
},
|
||||
{
|
||||
title: 'Branded URLs',
|
||||
description: 'Links mit deinem Benutzernamen für bessere Wiedererkennung',
|
||||
icon: '🏆'
|
||||
},
|
||||
{
|
||||
title: 'Custom QR Codes',
|
||||
description: 'Passe QR Code Farben und Formate an deine Marke an',
|
||||
icon: '🎨'
|
||||
},
|
||||
{
|
||||
title: 'Profil QR Codes',
|
||||
description: 'Teile dein Profil einfach per QR Code',
|
||||
icon: '📲'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Pro Features',
|
||||
icon: '⭐',
|
||||
items: [
|
||||
{
|
||||
title: 'Unbegrenzte Links',
|
||||
description: 'Keine Limits bei der Anzahl deiner Kurz-URLs',
|
||||
icon: '♾️'
|
||||
},
|
||||
{
|
||||
title: 'Erweiterte Analytics',
|
||||
description: 'Tiefgehende Einblicke und detaillierte Berichte',
|
||||
icon: '📊'
|
||||
},
|
||||
{
|
||||
title: 'Priority Support',
|
||||
description: 'Bevorzugter Support mit schnellen Antwortzeiten',
|
||||
icon: '🎖️'
|
||||
},
|
||||
{
|
||||
title: 'Keine Werbung',
|
||||
description: 'Werbefreie Nutzung für fokussiertes Arbeiten',
|
||||
icon: '🚫'
|
||||
},
|
||||
{
|
||||
title: 'Early Access',
|
||||
description: 'Sei der Erste bei neuen Features (Lifetime Plan)',
|
||||
icon: '🚀'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const comparisons = [
|
||||
{ feature: 'Links pro Monat', free: '10', pro: 'Unbegrenzt' },
|
||||
{ feature: 'Analytics', free: 'Basis', pro: 'Erweitert' },
|
||||
{ feature: 'QR Codes', free: 'Standard', pro: 'Anpassbar' },
|
||||
{ feature: 'Custom Short Codes', free: '✓', pro: '✓' },
|
||||
{ feature: 'Passwortschutz', free: '✓', pro: '✓' },
|
||||
{ feature: 'Ablaufdatum', free: '✓', pro: '✓' },
|
||||
{ feature: 'Priority Support', free: '✗', pro: '✓' },
|
||||
{ feature: 'Werbefrei', free: '✗', pro: '✓' },
|
||||
{ feature: 'Export Funktionen', free: '✗', pro: '✓' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Features - ulo.ad | Alle Funktionen im Überblick</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Entdecke alle Features von ulo.ad: URL-Verkürzung, QR-Codes, Analytics, Passwortschutz und mehr. Der moderne Link-Shortener für Profis."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<Navigation user={data.user} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<!-- Hero Section -->
|
||||
<div
|
||||
class="relative overflow-hidden bg-gradient-to-br from-theme-primary/10 to-theme-primary/5 px-4 py-16"
|
||||
>
|
||||
<div class="bg-grid-pattern absolute inset-0 opacity-5"></div>
|
||||
<div class="relative mx-auto max-w-7xl text-center">
|
||||
<h1 class="mb-6 text-5xl font-bold text-theme-text">
|
||||
Alles was du brauchst,<br />
|
||||
<span class="text-theme-primary">in einem Tool vereint</span>
|
||||
</h1>
|
||||
<p class="mx-auto mb-8 max-w-3xl text-xl text-theme-text-muted">
|
||||
Von einfacher URL-Verkürzung bis zu fortgeschrittenen Analytics und API-Integration. ulo.ad
|
||||
bietet dir alle Tools für professionelles Link-Management.
|
||||
</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<a
|
||||
href="/auth/register"
|
||||
class="rounded-lg bg-theme-primary px-6 py-3 font-medium text-theme-background transition-colors hover:bg-theme-primary-hover"
|
||||
>
|
||||
Jetzt starten
|
||||
</a>
|
||||
<a
|
||||
href="/pricing"
|
||||
class="rounded-lg bg-theme-surface px-6 py-3 font-medium text-theme-text transition-colors hover:bg-theme-surface-hover"
|
||||
>
|
||||
Preise ansehen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Community Roadmap Section -->
|
||||
<div class="bg-theme-surface/50 px-4 py-16">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<div class="mb-12 text-center">
|
||||
<h2 class="mb-4 text-3xl font-bold text-theme-text">🚀 Community Roadmap</h2>
|
||||
<p class="text-xl text-theme-text-muted">Stimme für die Features, die dir wichtig sind!</p>
|
||||
</div>
|
||||
|
||||
{#if filteredRequests.length > 0}
|
||||
<!-- Filters -->
|
||||
<div class="mb-8 flex flex-wrap justify-center gap-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => (statusFilter = 'all')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {statusFilter === 'all'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (statusFilter = 'new')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {statusFilter === 'new'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
Neu
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (statusFilter = 'planned')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {statusFilter === 'planned'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
Geplant
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (statusFilter = 'in_progress')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {statusFilter === 'in_progress'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
In Entwicklung
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => (sortBy = 'votes')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {sortBy === 'votes'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
🔥 Beliebteste
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (sortBy = 'newest')}
|
||||
class="rounded-lg px-4 py-2 transition-colors {sortBy === 'newest'
|
||||
? 'bg-theme-primary text-theme-background'
|
||||
: 'bg-theme-surface text-theme-text hover:bg-theme-surface-hover'}"
|
||||
>
|
||||
🆕 Neueste
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Requests List -->
|
||||
<div class="space-y-4">
|
||||
{#each filteredRequests as request}
|
||||
{@const isVoted = data.userVotedIds.includes(request.id)}
|
||||
{@const statusBadge = getStatusBadge(request.status)}
|
||||
{@const categoryIcon = getCategoryIcon(request.category)}
|
||||
|
||||
<div
|
||||
class="rounded-xl bg-theme-surface p-6 shadow-lg transition-shadow hover:shadow-xl"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<!-- Vote Button -->
|
||||
<div class="flex flex-col items-center">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/vote"
|
||||
use:enhance={() => {
|
||||
votingInProgress = request.id;
|
||||
return async ({ result, update }) => {
|
||||
votingInProgress = null;
|
||||
if (result.type === 'success') {
|
||||
// Optimistic update
|
||||
if (isVoted) {
|
||||
data.userVotedIds = data.userVotedIds.filter((id) => id !== request.id);
|
||||
request.vote_count = Math.max(0, (request.vote_count || 0) - 1);
|
||||
} else {
|
||||
data.userVotedIds = [...data.userVotedIds, request.id];
|
||||
request.vote_count = (request.vote_count || 0) + 1;
|
||||
}
|
||||
}
|
||||
update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="featureRequestId" value={request.id} />
|
||||
<input type="hidden" name="action" value={isVoted ? 'remove' : 'add'} />
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!data.user || votingInProgress === request.id}
|
||||
class="flex flex-col items-center gap-1 {isVoted
|
||||
? 'text-theme-primary'
|
||||
: 'text-theme-text-muted hover:text-theme-primary'}
|
||||
{!data.user ? 'cursor-not-allowed opacity-50' : 'transition-colors'}
|
||||
{votingInProgress === request.id ? 'animate-pulse' : ''}"
|
||||
title={!data.user
|
||||
? 'Zum Abstimmen anmelden'
|
||||
: isVoted
|
||||
? 'Stimme zurücknehmen'
|
||||
: 'Abstimmen'}
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 {isVoted ? 'fill-current' : ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-semibold">{request.vote_count || 0}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mb-2 text-lg font-semibold text-theme-text">
|
||||
{#if request.category}
|
||||
<span class="mr-2">{categoryIcon}</span>
|
||||
{/if}
|
||||
{request.message}
|
||||
</h3>
|
||||
{#if request.name}
|
||||
<p class="mb-2 text-sm text-theme-text-muted">
|
||||
Vorgeschlagen von {request.name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full px-3 py-1 text-xs font-medium {statusBadge.class}">
|
||||
{statusBadge.text}
|
||||
</span>
|
||||
{#if request.priority === 'critical'}
|
||||
<span
|
||||
class="rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
🔴 Kritisch
|
||||
</span>
|
||||
{:else if request.priority === 'high'}
|
||||
<span
|
||||
class="rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
|
||||
>
|
||||
🟠 Hoch
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-12 text-center">
|
||||
<p class="text-theme-text-muted">
|
||||
Noch keine Community-Vorschläge vorhanden. Sei der Erste!
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="px-4 py-16">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
{#each features as category}
|
||||
<div class="mb-16">
|
||||
<div class="mb-8 flex items-center gap-3">
|
||||
<span class="text-3xl">{category.icon}</span>
|
||||
<h2 class="text-3xl font-bold text-theme-text">{category.category}</h2>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
|
||||
>
|
||||
{#each category.items as item}
|
||||
{@const combinedText = `${item.title}. ${item.description}`}
|
||||
{@const firstSentenceEnd = combinedText.indexOf('.') + 1}
|
||||
{@const firstPart = combinedText.slice(0, firstSentenceEnd)}
|
||||
{@const secondPart = combinedText.slice(firstSentenceEnd).trim()}
|
||||
<div
|
||||
class="flex aspect-square transform flex-col items-start justify-start rounded-lg bg-theme-surface p-4 text-left transition-shadow duration-200 hover:scale-[1.02] hover:shadow-lg"
|
||||
>
|
||||
<span class="mb-3 text-2xl">{item.icon}</span>
|
||||
<p class="text-sm leading-relaxed text-theme-text">
|
||||
<span class="font-bold">{firstPart}</span>
|
||||
{#if secondPart}
|
||||
<span class="font-normal"> {secondPart}</span>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comparison Table -->
|
||||
<div class="bg-theme-surface/50 px-4 py-16">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<h2 class="mb-12 text-center text-3xl font-bold text-theme-text">Free vs Pro Vergleich</h2>
|
||||
<div class="overflow-hidden rounded-xl bg-theme-surface shadow-lg">
|
||||
<div class="grid grid-cols-3 bg-theme-primary text-theme-background">
|
||||
<div class="p-4 font-semibold">Feature</div>
|
||||
<div class="p-4 text-center font-semibold">Free</div>
|
||||
<div class="p-4 text-center font-semibold">Pro</div>
|
||||
</div>
|
||||
{#each comparisons as item, index}
|
||||
<div
|
||||
class="grid grid-cols-3 {index % 2 === 0
|
||||
? 'bg-theme-background'
|
||||
: 'bg-theme-surface/50'}"
|
||||
>
|
||||
<div class="p-4 font-medium text-theme-text">{item.feature}</div>
|
||||
<div class="p-4 text-center text-theme-text-muted">
|
||||
{#if item.free === '✓'}
|
||||
<span class="text-green-500">✓</span>
|
||||
{:else if item.free === '✗'}
|
||||
<span class="text-red-500">✗</span>
|
||||
{:else}
|
||||
{item.free}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="p-4 text-center text-theme-text">
|
||||
{#if item.pro === '✓'}
|
||||
<span class="font-bold text-green-500">✓</span>
|
||||
{:else}
|
||||
<span class="font-semibold text-theme-primary">{item.pro}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<div class="px-4 py-16">
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<h2 class="mb-12 text-center text-3xl font-bold text-theme-text">Warum ulo.ad?</h2>
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-4">
|
||||
<div class="text-center">
|
||||
<div class="mb-2 text-4xl font-bold text-theme-primary">99.9%</div>
|
||||
<div class="text-theme-text-muted">Uptime SLA</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-2 text-4xl font-bold text-theme-primary">~50ms</div>
|
||||
<div class="text-theme-text-muted">Avg. Response</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-2 text-4xl font-bold text-theme-primary">5</div>
|
||||
<div class="text-theme-text-muted">Sprachen</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-2 text-4xl font-bold text-theme-primary">24/7</div>
|
||||
<div class="text-theme-text-muted">Support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<div class="bg-gradient-to-r from-theme-primary/10 to-theme-primary/5 px-4 py-16">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<h2 class="mb-6 text-3xl font-bold text-theme-text">Bereit durchzustarten?</h2>
|
||||
<p class="mb-8 text-xl text-theme-text-muted">
|
||||
Starte kostenlos mit 10 Links pro Monat oder wähle einen Pro Plan für unbegrenzte
|
||||
Möglichkeiten.
|
||||
</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<a
|
||||
href="/auth/register"
|
||||
class="rounded-lg bg-theme-primary px-8 py-4 text-lg font-medium text-theme-background transition-colors hover:bg-theme-primary-hover"
|
||||
>
|
||||
Kostenlos starten
|
||||
</a>
|
||||
<a
|
||||
href="/pricing"
|
||||
class="rounded-lg bg-theme-surface px-8 py-4 text-lg font-medium text-theme-text transition-colors hover:bg-theme-surface-hover"
|
||||
>
|
||||
Pro Features ansehen
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-theme-text-muted">
|
||||
Keine Kreditkarte erforderlich • Jederzeit kündbar • DSGVO-konform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Request Form -->
|
||||
<div class="bg-theme-surface/50 px-4 py-16">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="rounded-xl bg-theme-surface p-8 shadow-lg">
|
||||
<h2 class="mb-2 text-2xl font-bold text-theme-text">Feedback & Wünsche</h2>
|
||||
<p class="mb-6 text-theme-text-muted">
|
||||
Hast du eine Idee oder vermisst du eine Funktion? Lass es uns wissen!
|
||||
</p>
|
||||
|
||||
{#if showSuccess}
|
||||
<div
|
||||
class="mb-6 rounded-lg border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-900/20"
|
||||
>
|
||||
<p class="text-green-800 dark:text-green-300">Vielen Dank für dein Feedback! 🎉</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/requestFeature"
|
||||
use:enhance={() => {
|
||||
isSubmitting = true;
|
||||
return async ({ result }) => {
|
||||
isSubmitting = false;
|
||||
if (result.type === 'success') {
|
||||
showSuccess = true;
|
||||
// Clear the form
|
||||
const form = document.getElementById('feedback-form') as HTMLFormElement;
|
||||
if (form) form.reset();
|
||||
setTimeout(() => (showSuccess = false), 5000);
|
||||
}
|
||||
};
|
||||
}}
|
||||
id="feedback-form"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label for="message" class="mb-2 block text-sm font-medium text-theme-text">
|
||||
Deine Nachricht
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows="5"
|
||||
class="w-full resize-none rounded-lg border border-theme-border bg-theme-background px-4 py-2 text-theme-text focus:border-transparent focus:ring-2 focus:ring-theme-primary"
|
||||
placeholder="Erzähl uns von deiner Idee oder deinem Feature-Wunsch..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<details class="group">
|
||||
<summary
|
||||
class="cursor-pointer text-sm text-theme-text-muted transition-colors hover:text-theme-text"
|
||||
>
|
||||
Optional: Kontaktdaten hinterlassen
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="name" class="mb-2 block text-sm font-medium text-theme-text">
|
||||
Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-background px-4 py-2 text-theme-text focus:border-transparent focus:ring-2 focus:ring-theme-primary"
|
||||
placeholder="Max Mustermann"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="mb-2 block text-sm font-medium text-theme-text">
|
||||
E-Mail (optional)
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-background px-4 py-2 text-theme-text focus:border-transparent focus:ring-2 focus:ring-theme-primary"
|
||||
placeholder="max@beispiel.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
class="w-full rounded-lg bg-theme-primary px-6 py-3 font-medium text-theme-background transition-colors hover:bg-theme-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Wird gesendet...' : 'Feedback absenden'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
# Feature-Voting-System Dokumentation
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Feature-Voting-System ermöglicht es Nutzern, Feature-Wünsche einzureichen und für Community-Vorschläge abzustimmen. Admins können diese Vorschläge moderieren und deren Status verwalten.
|
||||
|
||||
## Architektur
|
||||
|
||||
### Datenbank-Schema
|
||||
|
||||
#### 1. `feature_requests` Collection
|
||||
|
||||
Speichert alle Feature-Anfragen mit folgenden Feldern:
|
||||
|
||||
| Feld | Typ | Beschreibung | Pflichtfeld |
|
||||
| ------------ | -------- | -------------------------- | ----------- |
|
||||
| `id` | text | Eindeutige ID (15 Zeichen) | ✅ |
|
||||
| `message` | text | Die Feature-Beschreibung | ✅ |
|
||||
| `name` | text | Name des Einreichers | ❌ |
|
||||
| `email` | email | E-Mail des Einreichers | ❌ |
|
||||
| `status` | select | Status der Anfrage | ❌ |
|
||||
| `published` | bool | Ob öffentlich sichtbar | ❌ |
|
||||
| `vote_count` | number | Anzahl der Votes | ❌ |
|
||||
| `priority` | select | Priorität | ❌ |
|
||||
| `category` | select | Kategorie | ❌ |
|
||||
| `created` | autodate | Erstellungsdatum | Auto |
|
||||
| `updated` | autodate | Letztes Update | Auto |
|
||||
|
||||
**Status-Optionen:**
|
||||
|
||||
- `new` - Neue Anfrage
|
||||
- `reviewed` - In Prüfung
|
||||
- `planned` - Geplant
|
||||
- `in_progress` - In Entwicklung
|
||||
- `completed` - Fertiggestellt
|
||||
- `rejected` - Abgelehnt
|
||||
|
||||
**Prioritäts-Optionen:**
|
||||
|
||||
- `low` - Niedrig
|
||||
- `medium` - Mittel
|
||||
- `high` - Hoch
|
||||
- `critical` - Kritisch
|
||||
|
||||
**Kategorie-Optionen:**
|
||||
|
||||
- `ui` - User Interface
|
||||
- `performance` - Performance
|
||||
- `integration` - Integration
|
||||
- `security` - Sicherheit
|
||||
- `features` - Features
|
||||
- `other` - Sonstiges
|
||||
|
||||
**Zugriffsregeln:**
|
||||
|
||||
- **List/View:** Öffentlich (für published=true)
|
||||
- **Create:** Öffentlich (für Feedback-Formular)
|
||||
- **Update/Delete:** Nur Admins
|
||||
|
||||
#### 2. `feature_votes` Collection
|
||||
|
||||
Verwaltet die Abstimmungen der Nutzer:
|
||||
|
||||
| Feld | Typ | Beschreibung | Pflichtfeld |
|
||||
| ----------------- | -------- | ---------------------------- | ----------- |
|
||||
| `id` | text | Eindeutige ID | ✅ |
|
||||
| `user` | relation | Verweis auf users Collection | ✅ |
|
||||
| `feature_request` | relation | Verweis auf feature_requests | ✅ |
|
||||
| `created` | autodate | Zeitpunkt der Abstimmung | Auto |
|
||||
|
||||
**Zugriffsregeln:**
|
||||
|
||||
- **List/View:** Nur für eingeloggte Nutzer
|
||||
- **Create:** Nur für eingeloggte Nutzer (user muss auth.id sein)
|
||||
- **Update:** Nicht erlaubt
|
||||
- **Delete:** Nur eigene Votes
|
||||
|
||||
## Frontend-Komponenten
|
||||
|
||||
### 1. Feedback-Formular
|
||||
|
||||
**Ort:** Am Ende der Features-Seite
|
||||
|
||||
**Funktionen:**
|
||||
|
||||
- Einfaches Textfeld für Feedback
|
||||
- Optionale Kontaktdaten (Name & E-Mail)
|
||||
- Validierung und Erfolgs-Feedback
|
||||
- Automatisches Leeren nach Absenden
|
||||
|
||||
**Code-Snippet:**
|
||||
|
||||
```svelte
|
||||
<form method="POST" action="?/requestFeature">
|
||||
<textarea name="message" required />
|
||||
<details>
|
||||
<summary>Optional: Kontaktdaten</summary>
|
||||
<input type="text" name="name" />
|
||||
<input type="email" name="email" />
|
||||
</details>
|
||||
<button type="submit">Feedback absenden</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### 2. Community Roadmap
|
||||
|
||||
**Ort:** Nach dem Hero-Bereich der Features-Seite
|
||||
|
||||
**Komponenten:**
|
||||
|
||||
- **Filter-Buttons:** Status-Filter (Alle/Neu/Geplant/In Entwicklung)
|
||||
- **Sortierung:** Nach Votes oder Datum
|
||||
- **Feature-Karten:** Zeigen Vorschläge mit Vote-Button
|
||||
- **Vote-Button:** Interaktiv für eingeloggte Nutzer
|
||||
|
||||
**Features:**
|
||||
|
||||
- Optimistische Updates beim Voten
|
||||
- Echtzeit-Vote-Zähler
|
||||
- Status-Badges mit Farben
|
||||
- Kategorie-Icons
|
||||
- Prioritäts-Anzeige
|
||||
|
||||
### 3. Vote-Mechanismus
|
||||
|
||||
**Ablauf:**
|
||||
|
||||
1. Nutzer klickt auf Vote-Button
|
||||
2. Form wird per POST an `?/vote` Action gesendet
|
||||
3. Server prüft Authentifizierung
|
||||
4. Vote wird erstellt/gelöscht
|
||||
5. vote_count wird aktualisiert
|
||||
6. UI wird optimistisch aktualisiert
|
||||
|
||||
## Server-Actions
|
||||
|
||||
### 1. `requestFeature` Action
|
||||
|
||||
**Zweck:** Speichert neue Feature-Anfragen
|
||||
|
||||
**Ablauf:**
|
||||
|
||||
```typescript
|
||||
1. Formular-Daten validieren
|
||||
2. Feature-Request in PocketBase speichern
|
||||
3. Status auf 'new' setzen
|
||||
4. Erfolgs-Feedback zurückgeben
|
||||
```
|
||||
|
||||
### 2. `vote` Action
|
||||
|
||||
**Zweck:** Verwaltet Nutzer-Abstimmungen
|
||||
|
||||
**Ablauf:**
|
||||
|
||||
```typescript
|
||||
1. Authentifizierung prüfen
|
||||
2. Bei 'add':
|
||||
- Prüfen ob Vote existiert
|
||||
- Vote erstellen
|
||||
- vote_count erhöhen
|
||||
3. Bei 'remove':
|
||||
- Vote finden
|
||||
- Vote löschen
|
||||
- vote_count verringern
|
||||
```
|
||||
|
||||
## Admin-Workflow
|
||||
|
||||
### 1. Moderation neuer Anfragen
|
||||
|
||||
1. Admin öffnet PocketBase Admin-Panel
|
||||
2. Navigiert zu `feature_requests` Collection
|
||||
3. Filtert nach `published = false`
|
||||
4. Prüft neue Anfragen
|
||||
5. Setzt bei Freigabe:
|
||||
- `published = true`
|
||||
- `status` (z.B. "reviewed")
|
||||
- `category` (optional)
|
||||
- `priority` (optional)
|
||||
|
||||
### 2. Status-Verwaltung
|
||||
|
||||
Typischer Workflow:
|
||||
|
||||
```
|
||||
new → reviewed → planned → in_progress → completed
|
||||
↓
|
||||
rejected
|
||||
```
|
||||
|
||||
### 3. Vote-Count Management
|
||||
|
||||
- `vote_count` wird automatisch aktualisiert
|
||||
- Denormalisiert für Performance
|
||||
- Kann bei Bedarf manuell korrigiert werden
|
||||
|
||||
## Sicherheit
|
||||
|
||||
### Zugriffskontrollen
|
||||
|
||||
- **Öffentlich:** Ansicht der Roadmap, Feedback einreichen
|
||||
- **Eingeloggte Nutzer:** Abstimmen, eigene Votes verwalten
|
||||
- **Admins:** Moderation, Status-Verwaltung, alle Felder bearbeiten
|
||||
|
||||
### Validierung
|
||||
|
||||
- Server-seitige Validierung aller Eingaben
|
||||
- CSRF-Schutz durch SvelteKit
|
||||
- SQL-Injection-Schutz durch PocketBase
|
||||
- Rate-Limiting durch PocketBase
|
||||
|
||||
## Performance-Optimierungen
|
||||
|
||||
1. **Denormalisierte vote_count:** Vermeidet COUNT-Queries
|
||||
2. **Optimistische Updates:** Sofortiges UI-Feedback
|
||||
3. **Pagination:** Max. 50 Feature-Requests laden
|
||||
4. **Indexierung:** Auf user und feature_request in votes
|
||||
|
||||
## Erweiterungsmöglichkeiten
|
||||
|
||||
### Zukünftige Features
|
||||
|
||||
1. **E-Mail-Benachrichtigungen:**
|
||||
- Bei Status-Änderungen
|
||||
- Bei Implementierung
|
||||
|
||||
2. **Kommentare:**
|
||||
- Diskussion zu Feature-Requests
|
||||
- Admin-Antworten
|
||||
|
||||
3. **Anhänge:**
|
||||
- Screenshots
|
||||
- Mockups
|
||||
|
||||
4. **Export:**
|
||||
- CSV-Export für Admins
|
||||
- Roadmap als PDF
|
||||
|
||||
5. **API-Endpoints:**
|
||||
- Öffentliche API für Roadmap
|
||||
- Webhook für Status-Updates
|
||||
|
||||
## Wartung
|
||||
|
||||
### Regelmäßige Aufgaben
|
||||
|
||||
1. **Wöchentlich:**
|
||||
- Neue Anfragen moderieren
|
||||
- Status aktualisieren
|
||||
|
||||
2. **Monatlich:**
|
||||
- Implementierte Features auf "completed" setzen
|
||||
- Alte/irrelevante Anfragen archivieren
|
||||
|
||||
3. **Quartalsweise:**
|
||||
- Prioritäten überprüfen
|
||||
- Roadmap-Planung aktualisieren
|
||||
|
||||
### Backup
|
||||
|
||||
Feature-Requests sind Teil des regulären PocketBase-Backups.
|
||||
Wichtig: Beide Collections (`feature_requests` und `feature_votes`) sichern.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Votes werden nicht gezählt
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Prüfe ob user eingeloggt ist
|
||||
2. Prüfe feature_votes Collection Rules
|
||||
3. Manuell vote_count korrigieren
|
||||
|
||||
### Problem: Feature-Requests nicht sichtbar
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Prüfe `published = true`
|
||||
2. Prüfe Collection Rules
|
||||
3. Cache leeren
|
||||
|
||||
### Problem: Doppelte Votes
|
||||
|
||||
**Lösung:**
|
||||
|
||||
1. Unique Index auf user+feature_request erstellen
|
||||
2. Duplikate in DB entfernen
|
||||
3. vote_count neu berechnen
|
||||
|
||||
## Code-Referenzen
|
||||
|
||||
- **Frontend:** `/src/routes/features/+page.svelte`
|
||||
- **Server:** `/src/routes/features/+page.server.ts`
|
||||
- **Types:** Automatisch generiert in `./$types`
|
||||
- **PocketBase:** Collections `feature_requests` und `feature_votes`
|
||||
|
||||
## Deployment-Checklist
|
||||
|
||||
- [ ] PocketBase Collections erstellt
|
||||
- [ ] Zugriffsregeln konfiguriert
|
||||
- [ ] Admin-User angelegt
|
||||
- [ ] Erste Test-Features eingereicht
|
||||
- [ ] Vote-Funktionalität getestet
|
||||
- [ ] Moderation getestet
|
||||
- [ ] Mobile Ansicht geprüft
|
||||
- [ ] Performance getestet
|
||||
|
||||
---
|
||||
|
||||
_Letzte Aktualisierung: 14. August 2025_
|
||||
_Version: 1.0.0_
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData } from './$types';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import * as m from '$paraglide/messages';
|
||||
|
||||
let { form }: { form: ActionData } = $props();
|
||||
let isSubmitting = $state(false);
|
||||
let emailSent = $state(false);
|
||||
</script>
|
||||
|
||||
<Navigation user={null} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-theme-background p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<div class="mb-6 text-center">
|
||||
<svg
|
||||
class="mx-auto mb-4 h-12 w-12 text-theme-primary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="text-2xl font-bold text-theme-text">{m.auth_reset_password_title()}</h1>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
{m.auth_reset_password_subtitle()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if emailSent || form?.success}
|
||||
<div
|
||||
class="rounded-lg bg-green-50 p-4 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
<div class="flex">
|
||||
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">{m.auth_reset_email_sent_title()}</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_reset_email_sent_message()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<a href="/login" class="text-sm text-theme-accent hover:text-theme-accent-hover">
|
||||
{m.auth_back_to_login()}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/requestReset"
|
||||
use:enhance={() => {
|
||||
isSubmitting = true;
|
||||
return async ({ result, update }) => {
|
||||
await update();
|
||||
isSubmitting = false;
|
||||
if (result.type === 'success') {
|
||||
emailSent = true;
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-theme-text">
|
||||
{m.auth_email_address_label()}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder={m.auth_email_placeholder()}
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="animate-fade-in rounded-lg border border-red-400 bg-red-50 p-3 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
⚠️ {form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
class="flex w-full items-center justify-center rounded-lg bg-theme-primary px-4 py-3 font-medium text-white transition duration-200 hover:bg-theme-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<svg class="mr-2 h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{m.auth_send_reset_button_loading()}
|
||||
{:else}
|
||||
{m.auth_send_reset_button()}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t border-theme-border pt-6 text-center">
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
{m.auth_remember_password()}
|
||||
<a href="/login" class="font-medium text-theme-accent hover:text-theme-accent-hover">
|
||||
{m.auth_back_to_login()}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<Navigation user={data.user} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<h1 class="mb-8 text-3xl font-bold text-theme-text">Impressum</h1>
|
||||
|
||||
<div class="space-y-6 text-theme-text">
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">Angaben gemäß § 5 TMG</h2>
|
||||
<div class="space-y-1 text-theme-text-muted">
|
||||
<p>[Ihr Name oder Firmenname]</p>
|
||||
<p>[Ihre Straße und Hausnummer]</p>
|
||||
<p>[Ihre Postleitzahl und Ort]</p>
|
||||
<p>Deutschland</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">Kontakt</h2>
|
||||
<div class="space-y-1 text-theme-text-muted">
|
||||
<p>Telefon: [Ihre Telefonnummer]</p>
|
||||
<p>E-Mail: [Ihre E-Mail-Adresse]</p>
|
||||
<p>
|
||||
Website: {typeof window !== 'undefined' ? window.location.origin : 'https://uload.de'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">Umsatzsteuer-ID</h2>
|
||||
<div class="text-theme-text-muted">
|
||||
<p>Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:</p>
|
||||
<p>[Ihre USt-IdNr.]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">
|
||||
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
|
||||
</h2>
|
||||
<div class="space-y-1 text-theme-text-muted">
|
||||
<p>[Name des Verantwortlichen]</p>
|
||||
<p>[Adresse des Verantwortlichen]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">EU-Streitschlichtung</h2>
|
||||
<div class="text-theme-text-muted">
|
||||
<p>
|
||||
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS)
|
||||
bereit:
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://ec.europa.eu/consumers/odr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-theme-primary hover:underline">https://ec.europa.eu/consumers/odr/</a
|
||||
>
|
||||
</p>
|
||||
<p class="mt-2">Unsere E-Mail-Adresse finden Sie oben im Impressum.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">
|
||||
Verbraucherstreitbeilegung/Universalschlichtungsstelle
|
||||
</h2>
|
||||
<div class="text-theme-text-muted">
|
||||
<p>
|
||||
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">Haftungsausschluss</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Haftung für Inhalte</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten
|
||||
nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als
|
||||
Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde
|
||||
Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige
|
||||
Tätigkeit hinweisen.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Haftung für Links</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen
|
||||
Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr
|
||||
übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder
|
||||
Betreiber der Seiten verantwortlich.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">Urheberrecht</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen
|
||||
dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art
|
||||
der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen
|
||||
Zustimmung des jeweiligen Autors bzw. Erstellers.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 rounded-lg bg-yellow-50 p-4 text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400"
|
||||
>
|
||||
<p class="font-semibold">⚠️ Wichtiger Hinweis:</p>
|
||||
<p>
|
||||
Bitte ersetzen Sie alle Platzhalter in eckigen Klammern [ ] mit Ihren tatsächlichen Daten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
return {
|
||||
user
|
||||
};
|
||||
};
|
||||
|
|
@ -1,315 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { trackAuth } from '$lib/analytics';
|
||||
import * as m from '$paraglide/messages';
|
||||
import { toastMessages, notify } from '$lib/services/toast';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { form, data }: { form: ActionData; data: PageData } = $props();
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
// Check URL parameters for messages
|
||||
const justRegistered = $page.url.searchParams.get('registered') === 'true';
|
||||
const userEmail = $page.url.searchParams.get('email') || '';
|
||||
const emailVerified = $page.url.searchParams.get('verified') === 'true';
|
||||
const errorType = $page.url.searchParams.get('error');
|
||||
const note = $page.url.searchParams.get('note');
|
||||
const isAdditional = $derived(data?.isAdditional || false);
|
||||
|
||||
// Show toasts for URL parameters
|
||||
onMount(() => {
|
||||
if (emailVerified) {
|
||||
if (note === 'already-verified') {
|
||||
notify.info(m.auth_email_already_verified_notify(), m.auth_email_already_verified_notify_desc());
|
||||
} else {
|
||||
toastMessages.emailVerified();
|
||||
}
|
||||
} else if (errorType === 'token-expired') {
|
||||
notify.warning(m.auth_token_expired_notify(), m.auth_token_expired_notify_desc());
|
||||
} else if (justRegistered) {
|
||||
toastMessages.registerSuccess();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Navigation user={null} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-theme-background p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<div class="mb-6 text-center">
|
||||
<svg
|
||||
class="mx-auto mb-4 h-12 w-12 text-theme-primary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="text-2xl font-bold text-theme-text">
|
||||
{isAdditional ? m.auth_add_account() : m.auth_welcome_back()}
|
||||
</h1>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
{isAdditional ? m.auth_add_account_subtitle() : m.auth_welcome_back_subtitle()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if isAdditional}
|
||||
<div class="mb-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">{m.auth_add_account_info()}</h3>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
{m.auth_add_account_switch_info()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if emailVerified}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<svg
|
||||
class="mt-0.5 h-5 w-5 flex-shrink-0 text-green-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
{#if note === 'already-verified'}
|
||||
<p class="text-sm font-medium">
|
||||
{m.auth_email_already_verified()}
|
||||
</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_email_already_verified_message()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm font-medium">
|
||||
{m.auth_email_verified()}
|
||||
</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_email_verified_message()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if errorType === 'token-expired'}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-amber-200 bg-amber-50 p-4 text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<svg
|
||||
class="mt-0.5 h-5 w-5 flex-shrink-0 text-amber-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">
|
||||
{m.auth_verification_link_expired()}
|
||||
</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_verification_link_expired_message()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if errorType === 'invalid-token'}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-4 text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<svg
|
||||
class="mt-0.5 h-5 w-5 flex-shrink-0 text-red-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">
|
||||
{m.auth_invalid_verification_link()}
|
||||
</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_invalid_verification_link_message()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if justRegistered}
|
||||
<div
|
||||
class="mb-4 rounded-lg border border-blue-200 bg-blue-50 p-4 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<svg
|
||||
class="mt-0.5 h-5 w-5 flex-shrink-0 text-blue-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium">
|
||||
{m.auth_registration_success()}
|
||||
</p>
|
||||
<p class="mt-1 text-sm">
|
||||
{m.auth_registration_success_message({ email: userEmail })}
|
||||
</p>
|
||||
<p class="mt-3 text-xs opacity-75">
|
||||
{m.auth_registration_tip()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/login{isAdditional ? '&additional=true' : ''}"
|
||||
use:enhance={() => {
|
||||
isSubmitting = true;
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'redirect') {
|
||||
// Track successful login
|
||||
trackAuth('login', 'email');
|
||||
toastMessages.loginSuccess();
|
||||
// Reset submitting state before redirect
|
||||
isSubmitting = false;
|
||||
// Let the redirect happen
|
||||
await update();
|
||||
} else if (result.type === 'failure' && result.data?.error) {
|
||||
toastMessages.loginError(result.data.error);
|
||||
await update();
|
||||
isSubmitting = false;
|
||||
} else {
|
||||
await update();
|
||||
isSubmitting = false;
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-theme-text">
|
||||
{m.auth_email_label()}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label for="password" class="block text-sm font-medium text-theme-text">
|
||||
{m.auth_password_label()}
|
||||
</label>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
class="text-sm text-theme-accent hover:text-theme-accent-hover"
|
||||
>
|
||||
{m.auth_forgot_password()}
|
||||
</a>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="animate-fade-in rounded-lg border border-red-400 bg-red-50 p-3 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
⚠️ {form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
class="flex w-full items-center justify-center rounded-lg bg-theme-primary px-4 py-3 font-medium text-theme-background transition duration-200 hover:bg-theme-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<svg class="mr-2 h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{m.auth_login_button_loading()}
|
||||
{:else}
|
||||
{m.auth_login_button()}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t border-theme-border pt-6 text-center">
|
||||
{#if isAdditional}
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Noch keinen Account?
|
||||
<a href="/register?additional=true" class="font-medium text-theme-accent hover:text-theme-accent-hover"
|
||||
>Neuen Account erstellen</a
|
||||
>
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-theme-text-muted">
|
||||
<a href="/my" class="font-medium text-theme-accent hover:text-theme-accent-hover"
|
||||
>Zurück zu meinem Account</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
{m.auth_no_account()}
|
||||
<a href="/register" class="font-medium text-theme-accent hover:text-theme-accent-hover"
|
||||
>{m.auth_create_account()}</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { trackAuth } from '$lib/analytics';
|
||||
import * as m from '$paraglide/messages';
|
||||
|
||||
let { form, data }: { form: ActionData; data: PageData } = $props();
|
||||
let isSubmitting = $state(false);
|
||||
let isAdditionalAccount = $derived($page.url.searchParams.get('additional') === 'true');
|
||||
</script>
|
||||
|
||||
<Navigation user={null} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-theme-background p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<div class="mb-6 text-center">
|
||||
<svg
|
||||
class="mx-auto mb-4 h-12 w-12 text-theme-primary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
||||
/>
|
||||
</svg>
|
||||
<h1 class="text-2xl font-bold text-theme-text">{m.auth_create_account_title()}</h1>
|
||||
<p class="mt-2 text-theme-text-muted">{m.auth_create_account_subtitle()}</p>
|
||||
</div>
|
||||
|
||||
{#if isAdditionalAccount}
|
||||
<div class="mb-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">Zusätzlichen Account erstellen</h3>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
Du erstellst einen zusätzlichen Account. Nach der Registrierung kannst du zwischen deinen Accounts wechseln.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if data?.invitation}
|
||||
<div class="mb-6 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="h-5 w-5 text-green-600 dark:text-green-400 mt-0.5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-medium text-green-800 dark:text-green-200">Team Invitation</h3>
|
||||
<p class="mt-1 text-sm text-green-700 dark:text-green-300">
|
||||
{data.invitation.inviterName} has invited you to join their team. Create your account to accept.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/register{isAdditionalAccount ? '&additional=true' : ''}"
|
||||
use:enhance={() => {
|
||||
isSubmitting = true;
|
||||
console.log('[CLIENT] Starting registration submission');
|
||||
return async ({ result, update }) => {
|
||||
console.log('[CLIENT] Registration result:', result);
|
||||
console.log('[CLIENT] Result type:', result.type);
|
||||
if (result.type === 'failure') {
|
||||
console.error('[CLIENT] Registration failed with status:', result.status);
|
||||
console.error('[CLIENT] Error data:', result.data);
|
||||
if (result.data?.error) {
|
||||
console.error('[CLIENT] Error message:', result.data.error);
|
||||
}
|
||||
// Log all properties of result.data
|
||||
console.error('[CLIENT] All error properties:', JSON.stringify(result.data, null, 2));
|
||||
}
|
||||
await update();
|
||||
isSubmitting = false;
|
||||
// Track successful signup
|
||||
if (result.type === 'redirect') {
|
||||
console.log('[CLIENT] Registration successful, redirecting');
|
||||
trackAuth('signup', 'email');
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="mb-1 block text-sm font-medium text-theme-text">
|
||||
{m.auth_email_label()}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
value={data?.invitation?.email || ''}
|
||||
readonly={!!data?.invitation?.email}
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none {data?.invitation?.email ? 'bg-gray-100 dark:bg-gray-800' : ''}"
|
||||
/>
|
||||
{#if data?.invitation?.token}
|
||||
<input type="hidden" name="inviteToken" value={data.invitation.token} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="mb-1 block text-sm font-medium text-theme-text">
|
||||
{m.auth_password_label()}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="passwordConfirm" class="mb-1 block text-sm font-medium text-theme-text">
|
||||
{m.auth_password_confirm_label()}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full rounded-lg border border-theme-border bg-theme-surface px-3 py-2 text-theme-text placeholder-theme-text-muted focus:ring-2 focus:ring-theme-accent focus:outline-none"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-theme-text-muted">
|
||||
{m.auth_username_auto()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div
|
||||
class="animate-fade-in rounded-lg border border-red-400 bg-red-50 p-3 text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
⚠️ {form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
class="flex w-full items-center justify-center rounded-lg bg-theme-primary px-4 py-3 font-medium text-theme-background transition duration-200 hover:bg-theme-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if isSubmitting}
|
||||
<svg class="mr-2 h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{m.auth_register_button_loading()}
|
||||
{:else}
|
||||
{m.auth_register_button()}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t border-theme-border pt-6 text-center">
|
||||
{#if !isAdditionalAccount}
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
{m.auth_have_account()}
|
||||
<a href="/login" class="font-medium text-theme-accent hover:text-theme-accent-hover"
|
||||
>{m.auth_sign_in()}</a
|
||||
>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
<a href="/my" class="font-medium text-theme-accent hover:text-theme-accent-hover"
|
||||
>Zurück zu meinem Account</a
|
||||
>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<Navigation user={data.user} currentPath={$page.url.pathname} />
|
||||
|
||||
<div class="min-h-screen bg-theme-background">
|
||||
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="rounded-xl border border-theme-border bg-theme-surface p-8 shadow-xl">
|
||||
<h1 class="mb-8 text-3xl font-bold text-theme-text">Sicherheit</h1>
|
||||
|
||||
<div
|
||||
class="mb-6 rounded-lg bg-green-50 p-4 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
<p class="font-semibold">🔒 Ihre Sicherheit ist unsere Priorität</p>
|
||||
<p class="mt-1">
|
||||
Bei uload setzen wir modernste Sicherheitsstandards ein, um Ihre Daten und Links zu
|
||||
schützen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 text-theme-text">
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🛡️ Verschlüsselung</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">SSL/TLS-Verschlüsselung</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Alle Datenübertragungen zwischen Ihrem Browser und unseren Servern sind durch moderne
|
||||
SSL/TLS-Verschlüsselung geschützt. Wir verwenden ausschließlich TLS 1.3 und TLS 1.2 mit
|
||||
starken Cipher-Suites.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Verschlüsselte Speicherung</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Sensible Daten wie Passwörter werden mit branchenführenden Verschlüsselungsalgorithmen
|
||||
(bcrypt mit Salt) gespeichert. Selbst im unwahrscheinlichen Fall eines Datenlecks
|
||||
bleiben Ihre Passwörter geschützt.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Ende-zu-Ende Verschlüsselung für Premium-Nutzer</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Premium-Nutzer können optionale Ende-zu-Ende-Verschlüsselung für besonders sensible
|
||||
Links aktivieren. Diese Links können nur mit dem richtigen Schlüssel entschlüsselt
|
||||
werden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🔐 Authentifizierung & Zugriffskontrolle</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Sichere Authentifizierung</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>
|
||||
Starke Passwort-Anforderungen (mindestens 8 Zeichen, Groß-/Kleinbuchstaben, Zahlen)
|
||||
</li>
|
||||
<li>Zwei-Faktor-Authentifizierung (2FA) verfügbar</li>
|
||||
<li>Automatische Sitzungsbeendigung nach Inaktivität</li>
|
||||
<li>Schutz vor Brute-Force-Angriffen durch Rate-Limiting</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Passwortgeschützte Links</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Erstellen Sie passwortgeschützte Links für zusätzliche Sicherheit. Nur Personen mit dem
|
||||
korrekten Passwort können auf die Ziel-URL zugreifen.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">IP-Whitelisting für Enterprise</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Enterprise-Kunden können IP-Whitelisting aktivieren, um den Zugriff auf ihre Links nur
|
||||
von bestimmten IP-Adressen oder IP-Bereichen zu erlauben.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🛠️ Infrastruktur-Sicherheit</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Hosting & Server</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>Hosting in ISO 27001 zertifizierten Rechenzentren</li>
|
||||
<li>Redundante Server-Architektur für maximale Verfügbarkeit</li>
|
||||
<li>Regelmäßige Sicherheitsupdates und Patches</li>
|
||||
<li>24/7 Überwachung der Systemintegrität</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">DDoS-Schutz</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Unser Service ist durch einen fortschrittlichen DDoS-Schutz abgesichert, der Angriffe
|
||||
automatisch erkennt und abwehrt, um die Verfügbarkeit unseres Dienstes zu gewährleisten.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Web Application Firewall (WAF)</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Eine Web Application Firewall schützt vor gängigen Web-Angriffen wie SQL-Injection,
|
||||
Cross-Site-Scripting (XSS) und anderen OWASP Top 10 Bedrohungen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🔍 Überwachung & Schutz</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Malware & Phishing-Schutz</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Alle erstellten Links werden automatisch gegen bekannte Malware- und
|
||||
Phishing-Datenbanken geprüft. Verdächtige Links werden blockiert und zur manuellen
|
||||
Überprüfung markiert.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Echtzeit-Überwachung</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>Kontinuierliche Überwachung auf verdächtige Aktivitäten</li>
|
||||
<li>Automatische Erkennung von Missbrauchsmustern</li>
|
||||
<li>Sofortige Benachrichtigung bei Sicherheitsvorfällen</li>
|
||||
<li>Detaillierte Audit-Logs für Enterprise-Kunden</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Link-Validierung</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Regelmäßige Überprüfung aller Ziel-URLs auf Verfügbarkeit und Sicherheit. Gefährliche
|
||||
oder kompromittierte Websites werden automatisch blockiert.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">📊 Datenschutz & Compliance</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">DSGVO-Konformität</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Vollständige Einhaltung der Datenschutz-Grundverordnung (DSGVO). Sie haben jederzeit die
|
||||
volle Kontrolle über Ihre Daten mit Rechten auf Auskunft, Berichtigung und Löschung.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Datensparsamkeit</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Wir sammeln nur die minimal notwendigen Daten für den Betrieb unseres Services. Keine
|
||||
unnötige Datensammlung oder -weitergabe an Dritte.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Regelmäßige Audits</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Unabhängige Sicherheitsaudits und Penetrationstests werden regelmäßig durchgeführt, um
|
||||
höchste Sicherheitsstandards zu gewährleisten.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🔄 Backup & Wiederherstellung</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Automatische Backups</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>Tägliche automatische Backups aller Daten</li>
|
||||
<li>Geografisch verteilte Backup-Speicherung</li>
|
||||
<li>Verschlüsselte Backup-Archive</li>
|
||||
<li>Regelmäßige Wiederherstellungstests</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Disaster Recovery</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Umfassender Disaster-Recovery-Plan mit RPO (Recovery Point Objective) von maximal 24
|
||||
Stunden und RTO (Recovery Time Objective) von maximal 4 Stunden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">👤 Ihre Verantwortung</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Best Practices für Nutzer</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-theme-text-muted">
|
||||
<li>Verwenden Sie starke, einzigartige Passwörter</li>
|
||||
<li>Aktivieren Sie die Zwei-Faktor-Authentifizierung</li>
|
||||
<li>Teilen Sie Ihre Zugangsdaten niemals mit anderen</li>
|
||||
<li>Melden Sie verdächtige Aktivitäten sofort</li>
|
||||
<li>Halten Sie Ihre Kontaktinformationen aktuell</li>
|
||||
<li>Überprüfen Sie regelmäßig Ihre Account-Aktivitäten</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">🚨 Sicherheitsvorfälle melden</h2>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Verantwortungsvolle Offenlegung</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Wir schätzen die Arbeit von Sicherheitsforschern. Wenn Sie eine Sicherheitslücke
|
||||
entdecken, melden Sie diese bitte verantwortungsvoll an:
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<p class="font-mono text-sm text-theme-text">security@uload.de</p>
|
||||
</div>
|
||||
<p class="mt-2 text-theme-text-muted">
|
||||
Bitte geben Sie uns angemessene Zeit zur Behebung, bevor Sie die Schwachstelle
|
||||
öffentlich machen.
|
||||
</p>
|
||||
|
||||
<h3 class="mt-4 mb-2 font-semibold">Bug Bounty Programm</h3>
|
||||
<p class="text-theme-text-muted">
|
||||
Für kritische Sicherheitslücken bieten wir Belohnungen im Rahmen unseres Bug Bounty
|
||||
Programms. Details finden Sie unter:
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-100 p-3 dark:bg-gray-800">
|
||||
<p class="font-mono text-sm text-theme-text">uload.de/bug-bounty</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-xl font-semibold">📋 Zertifizierungen & Standards</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-theme-border p-4">
|
||||
<h3 class="mb-2 font-semibold">ISO 27001</h3>
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Informationssicherheits-Management-System zertifiziert
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-border p-4">
|
||||
<h3 class="mb-2 font-semibold">SSL Labs A+</h3>
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Höchste Bewertung für SSL/TLS-Konfiguration
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-border p-4">
|
||||
<h3 class="mb-2 font-semibold">OWASP Compliance</h3>
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Einhaltung der OWASP-Sicherheitsrichtlinien
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-theme-border p-4">
|
||||
<h3 class="mb-2 font-semibold">PCI DSS Ready</h3>
|
||||
<p class="text-sm text-theme-text-muted">
|
||||
Bereit für Payment Card Industry Standards
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="mb-3 text-xl font-semibold">📞 Kontakt</h2>
|
||||
<p class="text-theme-text-muted">
|
||||
Bei Fragen zur Sicherheit unseres Services kontaktieren Sie uns:
|
||||
</p>
|
||||
<div class="mt-3 space-y-2">
|
||||
<p class="text-theme-text-muted">
|
||||
<span class="font-semibold">E-Mail:</span> security@uload.de
|
||||
</p>
|
||||
<p class="text-theme-text-muted">
|
||||
<span class="font-semibold">PGP-Schlüssel:</span> Verfügbar auf Anfrage
|
||||
</p>
|
||||
<p class="text-theme-text-muted">
|
||||
<span class="font-semibold">Notfall-Hotline:</span> +49 (0) [Telefonnummer]
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-8 rounded-lg bg-blue-50 p-4 text-sm text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
||||
>
|
||||
<p class="font-semibold">💡 Tipp:</p>
|
||||
<p>
|
||||
Aktivieren Sie die Zwei-Faktor-Authentifizierung in Ihren Account-Einstellungen für
|
||||
maximale Sicherheit!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 rounded-lg bg-green-50 p-4 text-sm text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
<p class="font-semibold">✓ Letzte Sicherheitsüberprüfung:</p>
|
||||
<p>
|
||||
{new Date().toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
return {
|
||||
user
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue