Feat: New project chat, uload refactor (postgress), hosting plans, uload landingpage

This commit is contained in:
Till-JS 2025-11-25 13:01:41 +01:00
parent 559eb08d8c
commit fcf3a344b1
123 changed files with 7106 additions and 3715 deletions

View file

@ -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",

View file

@ -1 +0,0 @@
cache

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -1 +0,0 @@
vBR0K1t5zNgjHxICus

View file

@ -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"
}
}

View file

@ -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;

View file

@ -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={() => {

View file

@ -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

View 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()}

View file

@ -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'
}}
/>

View 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...'
}}
/>

View 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.'
}}
/>

View file

@ -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>

View file

@ -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.'
};
};

View file

@ -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 />

View file

@ -1,8 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
return {
user
};
};

View 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()
});
};

View file

@ -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
};
};

View file

@ -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 />

View file

@ -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} />

View file

@ -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
};
};

View file

@ -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;

View file

@ -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 />

View file

@ -1,8 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
return {
user
};
};

View file

@ -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.' });
}
}
};

View file

@ -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>

View file

@ -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_

View file

@ -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>

View file

@ -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 />

View file

@ -1,8 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
return {
user
};
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1,8 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ parent }) => {
const { user } = await parent();
return {
user
};
};