This commit is contained in:
MeinholC 2026-04-14 18:28:26 +02:00
parent 79755d2421
commit 603db35457
14 changed files with 815 additions and 620 deletions

View File

@ -71,7 +71,62 @@ class AuthController extends Controller
}
/**
* Logout - Token löschen.
* Web-Login mit Session (für Blade-Views).
*/
public function webLogin(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended('/');
}
return back()->withErrors([
'email' => 'Die eingegebenen Anmeldedaten sind ungültig.',
])->onlyInput('email');
}
/**
* Web-Registrierung mit Session.
*/
public function webRegister(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
'role' => 'user',
]);
Auth::login($user);
$request->session()->regenerate();
return redirect('/');
}
/**
* Web-Logout (Session beenden).
*/
public function webLogout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
/**
* Logout - Token löschen (API).
*/
public function logout(Request $request)
{

View File

@ -55,9 +55,10 @@ class EventWebController extends Controller
$locations = Event::published()
->whereNotNull('location_id')
->with('location')
->distinct()
->get()
->pluck('location.name')
->pluck('location.city')
->filter()
->unique()
->sort()
->values();

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class ProfileController extends Controller
{
public function show()
{
return view('profile', ['user' => Auth::user()]);
}
public function updateInfo(Request $request)
{
$user = Auth::user();
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email,' . $user->id],
]);
$user->update($validated);
return redirect()->route('profile')->with('success_info', 'Name und EMail wurden gespeichert.');
}
public function updatePassword(Request $request)
{
$request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', 'confirmed', Password::min(8)],
]);
Auth::user()->update([
'password' => Hash::make($request->password),
]);
return redirect()->route('profile')->with('success_pw', 'Passwort wurde erfolgreich geändert.');
}
}

View File

@ -116,8 +116,7 @@ class Event extends Model
public function scopeByLocation($query, $location)
{
return $query->whereHas('location', function ($q) use ($location) {
$q->where('name', 'like', '%' . $location . '%')
->orWhere('city', 'like', '%' . $location . '%');
$q->where('city', $location);
});
}

View File

@ -1,191 +1,123 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Login - {{ config('app.name', 'Veranstaltungen') }}</title>
<!-- Fonts -->
<title>Anmelden Veranstaltungs-App</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600,700" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Instrument Sans', sans-serif;
background: linear-gradient(135deg, #f0f4ff 0%, #faf5ff 100%);
min-height: 100vh;
display: flex; align-items: center; justify-content: center;
padding: 24px;
}
.card {
background: white; border-radius: 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.10);
padding: 40px 36px; width: 100%; max-width: 420px;
}
.logo-link {
display: block; text-align: center;
font-size: 1.3em; font-weight: 800; color: #1a1a2e;
text-decoration: none; margin-bottom: 8px; letter-spacing: -0.02em;
}
.logo-link span { color: #667eea; }
h1 { font-size: 1.5em; font-weight: 700; color: #1a1a2e; text-align: center; margin-bottom: 4px; }
.subtitle { text-align: center; color: #9ca3af; font-size: 0.875em; margin-bottom: 28px; }
.field { margin-bottom: 18px; }
label { display: block; font-size: 0.85em; font-weight: 600; color: #374151; margin-bottom: 6px; }
input[type="email"], input[type="password"] {
width: 100%; padding: 10px 14px;
border: 1.5px solid #e5e7eb; border-radius: 8px;
font-size: 0.9em; color: #1a1a2e;
transition: border-color 0.2s, box-shadow 0.2s; outline: none;
}
input:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,0.15); }
.row-between {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 22px; gap: 10px;
}
.remember-label { display: flex; align-items: center; gap: 6px; font-size: 0.82em; color: #6b7280; cursor: pointer; }
.forgot-link { font-size: 0.82em; color: #667eea; text-decoration: none; font-weight: 600; }
.forgot-link:hover { text-decoration: underline; }
.btn-submit {
width: 100%; padding: 11px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; font-size: 0.95em; font-weight: 700;
border: none; border-radius: 8px; cursor: pointer;
transition: opacity 0.2s; margin-bottom: 16px;
}
.btn-submit:hover { opacity: 0.9; }
.divider { border: none; border-top: 1px solid #f3f4f6; margin: 20px 0; }
.register-hint { text-align: center; font-size: 0.85em; color: #6b7280; }
.register-hint a { color: #667eea; font-weight: 600; text-decoration: none; }
.register-hint a:hover { text-decoration: underline; }
.demo-box {
margin-top: 20px; background: #f0f4ff;
border: 1px solid #c7d2fe; border-radius: 8px; padding: 12px 14px;
}
.demo-box p { font-size: 0.78em; color: #4338ca; font-weight: 600; margin-bottom: 6px; }
.demo-box span { display: block; font-size: 0.78em; color: #4f46e5; margin-bottom: 2px; }
.alert-error {
background: #fef2f2; border: 1px solid #fecaca;
border-radius: 8px; padding: 12px 14px; margin-bottom: 18px;
}
.alert-error p { font-size: 0.83em; color: #dc2626; }
</style>
</head>
<body class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 min-h-screen flex items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<!-- Logo / Header -->
<div class="text-center mb-8">
<a href="{{ url('/') }}" class="inline-flex items-center gap-2">
<div class="w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</a>
<h1 class="mt-6 text-3xl font-bold text-gray-900 dark:text-white">Anmelden</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Melden Sie sich in Ihrem Konto an, um fortzufahren
</p>
</div>
<body>
<div class="card">
<a href="{{ url('/') }}" class="logo-link">Veranstaltungs<span>-App</span></a>
<h1>Willkommen zurück</h1>
<p class="subtitle">Melden Sie sich an, um fortzufahren</p>
<!-- Form -->
<div class="bg-white dark:bg-gray-800 shadow-lg rounded-lg p-8">
<form id="loginForm" class="space-y-6" @submit.prevent="handleLogin">
<!-- Email Field -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
E-Mail Adresse
</label>
<input
type="email"
id="email"
name="email"
placeholder="name@example.com"
required
autocomplete="email"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white transition"
/>
<span class="error-message text-red-600 dark:text-red-400 text-sm mt-1 hidden"></span>
</div>
<!-- Password Field -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Passwort
</label>
<input
type="password"
id="password"
name="password"
placeholder="Ihr Passwort"
required
autocomplete="current-password"
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white transition"
/>
<span class="error-message text-red-600 dark:text-red-400 text-sm mt-1 hidden"></span>
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between">
<label class="flex items-center gap-2">
<input type="checkbox" name="remember" class="w-4 h-4 border border-gray-300 dark:border-gray-600 rounded accent-blue-600">
<span class="text-sm text-gray-600 dark:text-gray-400">Anmeldedaten speichern</span>
</label>
<a href="{{ url('/forgot-password') }}" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
Passwort vergessen?
</a>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800 text-white font-semibold py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center gap-2"
>
<span class="loading hidden">
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" 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>
</span>
<span class="text">Anmelden</span>
</button>
<!-- Success/Error Message -->
<div id="messageContainer"></div>
</form>
<!-- Divider -->
<div class="mt-8 border-t border-gray-300 dark:border-gray-700"></div>
<!-- Register Link -->
<div class="mt-6 text-center">
<p class="text-sm text-gray-600 dark:text-gray-400">
Noch kein Konto?
<a href="{{ url('/register') }}" class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-semibold">
Jetzt registrieren
</a>
</p>
@if ($errors->any())
<div class="alert-error">
@foreach ($errors->all() as $error)
<p>{{ $error }}</p>
@endforeach
</div>
@endif
<!-- Demo Users Info -->
<div class="mt-6 p-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg">
<p class="text-xs font-semibold text-blue-900 dark:text-blue-200 mb-2">Demo Konten:</p>
<div class="space-y-1 text-xs text-blue-800 dark:text-blue-300">
<p>👤 user@example.com / password123</p>
<p>🎭 organizer@example.com / password123</p>
<p>👨‍💼 admin@example.com / password123</p>
</div>
<form method="POST" action="{{ route('login.post') }}">
@csrf
<div class="field">
<label for="email">E-Mail Adresse</label>
<input type="email" id="email" name="email"
value="{{ old('email') }}"
placeholder="name@example.com"
required autocomplete="email">
</div>
<div class="field">
<label for="password">Passwort</label>
<input type="password" id="password" name="password"
placeholder="Ihr Passwort"
required autocomplete="current-password">
</div>
<div class="row-between">
<label class="remember-label">
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
Angemeldet bleiben
</label>
<a href="{{ route('forgot-password') }}" class="forgot-link">Passwort vergessen?</a>
</div>
<button type="submit" class="btn-submit">Anmelden</button>
</form>
<hr class="divider">
<p class="register-hint">
Noch kein Konto? <a href="{{ route('register') }}">Jetzt registrieren</a>
</p>
<div class="demo-box">
<p>Demo-Konten:</p>
<span>👤 user@example.com / password123</span>
<span>🎭 organizer@example.com / password123</span>
<span>👨‍💼 admin@example.com / password123</span>
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const btn = e.target.querySelector('button[type="submit"]');
const loadingSpan = btn.querySelector('.loading');
const textSpan = btn.querySelector('.text');
// Show loading state
loadingSpan.classList.remove('hidden');
textSpan.textContent = 'Wird angemeldet...';
btn.disabled = true;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.success) {
// Store token
localStorage.setItem('auth_token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
// Show success message
showMessage('Erfolgreich angemeldet! Sie werden weitergeleitet...', 'success');
// Redirect after 1.5 seconds
setTimeout(() => {
window.location.href = '/';
}, 1500);
} else {
const errorMsg = data.message || 'Anmeldung fehlgeschlagen';
showMessage(errorMsg, 'error');
}
} catch (error) {
showMessage('Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut.', 'error');
console.error('Login error:', error);
} finally {
loadingSpan.classList.add('hidden');
textSpan.textContent = 'Anmelden';
btn.disabled = false;
}
});
function showMessage(message, type) {
const container = document.getElementById('messageContainer');
const bgColor = type === 'success' ? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800' : 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800';
const textColor = type === 'success' ? 'text-green-800 dark:text-green-300' : 'text-red-800 dark:text-red-300';
container.innerHTML = `
<div class="p-4 ${bgColor} border rounded-lg">
<p class="${textColor} text-sm font-medium">${message}</p>
</div>
`;
}
</script>
</body>
</html>

View File

@ -3,328 +3,155 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $event->title }} - Veranstaltungen</title>
<title>{{ $event->title }} - Veranstaltungs-App</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.back-link {
display: inline-block;
color: white;
text-decoration: none;
margin-bottom: 30px;
font-weight: 600;
transition: transform 0.2s;
}
.back-link:hover {
transform: translateX(-5px);
}
.detail-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.detail-image {
width: 100%;
height: 400px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 8em;
position: relative;
overflow: hidden;
}
.detail-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.detail-content {
padding: 50px;
}
.event-category {
display: inline-block;
background: #f0f0f0;
color: #667eea;
padding: 8px 20px;
border-radius: 25px;
font-size: 0.9em;
font-weight: 600;
margin-bottom: 20px;
}
.event-title {
font-size: 2.5em;
font-weight: 800;
color: #333;
margin-bottom: 30px;
line-height: 1.2;
}
.event-source {
display: inline-block;
background: #e8f0ff;
color: #667eea;
padding: 8px 15px;
border-radius: 5px;
font-size: 0.85em;
margin-bottom: 30px;
margin-left: 20px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
margin-bottom: 40px;
padding-bottom: 40px;
border-bottom: 2px solid #f0f0f0;
}
.info-box {
display: flex;
gap: 15px;
}
.info-icon {
font-size: 2em;
min-width: 40px;
}
.info-text h4 {
font-size: 0.85em;
text-transform: uppercase;
color: #999;
font-weight: 600;
margin-bottom: 5px;
letter-spacing: 0.5px;
}
.info-text p {
font-size: 1.1em;
color: #333;
font-weight: 600;
line-height: 1.4;
}
.section-title {
font-size: 1.5em;
font-weight: 700;
color: #333;
margin-top: 40px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 3px solid #667eea;
}
.description {
color: #555;
font-size: 1.05em;
line-height: 1.8;
margin-bottom: 40px;
}
.occurrences {
background: #f8f9ff;
padding: 25px;
border-radius: 10px;
margin-bottom: 40px;
}
.occurrence-item {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
}
.occurrence-item:last-child {
margin-bottom: 0;
}
.occurrence-date {
font-weight: 700;
font-size: 1.1em;
color: #333;
}
.occurrence-time {
color: #999;
font-size: 0.9em;
}
.occurrence-status {
display: inline-block;
background: #e8f5e9;
color: #2e7d32;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 50px;
flex-wrap: wrap;
}
.btn {
padding: 15px 40px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 1em;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f0f0f0;
color: #333;
border: 2px solid #e0e0e0;
}
.btn-secondary:hover {
background: #e8e8e8;
}
.contact-info {
background: #f8f9ff;
padding: 25px;
border-radius: 10px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.contact-item {
display: flex;
gap: 15px;
}
.contact-item strong {
color: #333;
}
.contact-item a {
color: #667eea;
text-decoration: none;
}
.contact-item a:hover {
text-decoration: underline;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9f9f9; color: #1a1a2e; }
main { max-width: 900px; margin: 0 auto; padding: 40px 20px; }
.back-link { display: inline-flex; align-items: center; gap: 6px; color: #667eea; text-decoration: none; font-size: 0.875em; font-weight: 600; margin-bottom: 24px; transition: gap 0.2s; }
.back-link:hover { gap: 10px; }
.detail-card { background: white; border-radius: 16px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.08); border: 1px solid #eee; }
.detail-stripe { height: 6px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.detail-content { padding: 40px; }
.meta-top { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.category-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; background: #f0f0ff; color: #4338ca; }
.source-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.75em; font-weight: 600; background: #f0fdf4; color: #15803d; }
h1 { font-size: 2.2em; font-weight: 800; line-height: 1.2; margin-bottom: 32px; color: #1a1a2e; }
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 36px; padding-bottom: 36px; border-bottom: 1px solid #f3f4f6; }
.info-box { background: #f9f9f9; border-radius: 10px; padding: 16px 20px; display: flex; gap: 14px; align-items: flex-start; }
.info-icon { font-size: 1.5em; line-height: 1; }
.info-text label { display: block; font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #9ca3af; margin-bottom: 4px; }
.info-text p { font-size: 0.95em; font-weight: 600; color: #1a1a2e; line-height: 1.4; }
.section-title { font-size: 1.15em; font-weight: 700; color: #1a1a2e; margin-bottom: 16px; padding-bottom: 10px; border-bottom: 2px solid #667eea; display: inline-block; }
.description { color: #4b5563; font-size: 1em; line-height: 1.8; margin-bottom: 40px; }
.occurrences { display: flex; flex-direction: column; gap: 10px; margin-bottom: 40px; }
.occurrence-item { background: #f9f9f9; border-left: 3px solid #667eea; border-radius: 0 8px 8px 0; padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.occurrence-date { font-weight: 700; font-size: 0.95em; }
.occurrence-time { color: #6b7280; font-size: 0.85em; margin-top: 2px; }
.occurrence-status { font-size: 0.75em; font-weight: 700; background: #ecfdf5; color: #15803d; padding: 3px 10px; border-radius: 20px; white-space: nowrap; }
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 40px; }
.contact-item { background: #f9f9f9; border-radius: 10px; padding: 16px 20px; }
.contact-item label { display: block; font-size: 0.72em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #9ca3af; margin-bottom: 6px; }
.contact-item a, .contact-item span { font-size: 0.9em; color: #667eea; font-weight: 600; text-decoration: none; }
.contact-item a:hover { text-decoration: underline; }
.action-buttons { display: flex; gap: 12px; margin-top: 40px; flex-wrap: wrap; }
.btn-primary { padding: 12px 28px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; text-decoration: none; font-weight: 700; font-size: 0.9em; transition: opacity 0.2s; }
.btn-primary:hover { opacity: 0.9; }
.btn-secondary { padding: 12px 28px; background: #f3f4f6; color: #374151; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 0.9em; transition: background 0.2s; }
.btn-secondary:hover { background: #e5e7eb; }
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link"> Zurück zur Übersicht</a>
@include('partials.header')
<main>
<a href="{{ route('events') }}" class="back-link"> Zurück zur Übersicht</a>
<div class="detail-card">
<div class="detail-image">
@if($event->image_url)
<img src="{{ $event->image_url }}" alt="{{ $event->title }}">
@else
<span>🎭</span>
@endif
</div>
<div class="detail-stripe"></div>
<div class="detail-content">
@if($event->category)
<span class="event-category">{{ $event->category }}</span>
@endif
@if($event->source)
<span class="event-source">Quelle: {{ $event->source->name }}</span>
@endif
<h1 class="event-title">{{ $event->title }}</h1>
<div class="meta-top">
@if($event->category)
<span class="category-badge">{{ $event->category }}</span>
@endif
@if($event->source)
<span class="source-badge">Quelle: {{ $event->source->name }}</span>
@endif
</div>
<h1>{{ $event->title }}</h1>
<div class="info-grid">
@if($event->occurrences->count() > 0)
@php $firstOccurrence = $event->occurrences->first(); @endphp
@php $first = $event->occurrences->first(); @endphp
<div class="info-box">
<div class="info-icon">📅</div>
<span class="info-icon">📅</span>
<div class="info-text">
<h4>Nächster Termin</h4>
<p>{{ $firstOccurrence->start_datetime->format('d. F Y') }}</p>
<label>Nächster Termin</label>
<p>{{ $first->start_datetime->format('d. F Y') }}</p>
</div>
</div>
<div class="info-box">
<div class="info-icon"></div>
<span class="info-icon"></span>
<div class="info-text">
<h4>Uhrzeit</h4>
<p>{{ $firstOccurrence->start_datetime->format('H:i') }}
@if($firstOccurrence->end_datetime)
- {{ $firstOccurrence->end_datetime->format('H:i') }}
@endif
Uhr
</p>
<label>Uhrzeit</label>
<p>{{ $first->start_datetime->format('H:i') }}{{ $first->end_datetime ? ' ' . $first->end_datetime->format('H:i') : '' }} Uhr</p>
</div>
</div>
@endif
@if($event->location)
<div class="info-box">
<div class="info-icon">📍</div>
<span class="info-icon">📍</span>
<div class="info-text">
<h4>Ort</h4>
<p>{{ $event->location->name }}<br>{{ $event->location->city }}</p>
<label>Ort</label>
<p>{{ $event->location->name }}, {{ $event->location->city }}</p>
</div>
</div>
@endif
</div>
@if($event->description)
<h2 class="section-title">Beschreibung</h2>
<div class="section-title">Beschreibung</div>
<p class="description">{{ $event->description }}</p>
@endif
@if($event->occurrences->count() > 0)
<h2 class="section-title">Alle Termine ({{ $event->occurrences->count() }})</h2>
<div class="section-title">Alle Termine ({{ $event->occurrences->count() }})</div>
<div class="occurrences">
@foreach($event->occurrences as $occurrence)
@foreach($event->occurrences as $occ)
<div class="occurrence-item">
<div>
<div class="occurrence-date">
📅 {{ $occurrence->start_datetime->format('d. F Y') }}
</div>
<div class="occurrence-time">
{{ $occurrence->start_datetime->format('H:i') }}
@if($occurrence->end_datetime)
- {{ $occurrence->end_datetime->format('H:i') }}
@endif
Uhr
</div>
<div class="occurrence-date">{{ $occ->start_datetime->format('d. F Y') }}</div>
<div class="occurrence-time">{{ $occ->start_datetime->format('H:i') }}{{ $occ->end_datetime ? ' ' . $occ->end_datetime->format('H:i') : '' }} Uhr</div>
</div>
<span class="occurrence-status">{{ ucfirst($occurrence->status) }}</span>
@php
$statusLabels = [
'scheduled' => 'Geplant',
'cancelled' => 'Abgesagt',
'postponed' => 'Verschoben',
'sold_out' => 'Ausverkauft',
'completed' => 'Abgeschlossen',
];
$statusLabel = $statusLabels[$occ->status] ?? ucfirst($occ->status);
@endphp
<span class="occurrence-status">{{ $statusLabel }}</span>
</div>
@endforeach
</div>
@endif
@if($event->contact_email || $event->contact_phone || $event->website_url)
<h2 class="section-title">Kontakt & Links</h2>
<div class="contact-info">
<div class="section-title">Kontakt & Links</div>
<div class="contact-grid">
@if($event->contact_email)
<div class="contact-item">
<strong>✉️ Email:</strong>
<label>✉️ E-Mail</label>
<a href="mailto:{{ $event->contact_email }}">{{ $event->contact_email }}</a>
</div>
@endif
@if($event->contact_phone)
<div class="contact-item">
<strong>📞 Telefon:</strong>
<label>📞 Telefon</label>
<a href="tel:{{ $event->contact_phone }}">{{ $event->contact_phone }}</a>
</div>
@endif
@if($event->website_url)
<div class="contact-item">
<strong>🌐 Website:</strong>
<a href="{{ $event->website_url }}" target="_blank" rel="noopener">Zur Website</a>
<label>🌐 Website</label>
<a href="{{ $event->website_url }}" target="_blank" rel="noopener">Zur Website </a>
</div>
@endif
</div>
@ -332,12 +159,15 @@
<div class="action-buttons">
@if($event->website_url)
<a href="{{ $event->website_url }}" class="btn btn-primary" target="_blank">🎫 Jetzt Tickets buchen</a>
<a href="{{ $event->website_url }}" class="btn-primary" target="_blank">🎫 Tickets / Mehr Info</a>
@endif
<a href="/" class="btn btn-secondary">Mehr Veranstaltungen entdecken</a>
<a href="{{ route('events') }}" class="btn-secondary"> Alle Veranstaltungen</a>
</div>
</div>
</div>
</div>
</main>
@include('partials.footer')
</body>
</html>

View File

@ -108,7 +108,7 @@
<span>📍 {{ $event->location->name }}</span>
@endif
</div>
<a href="{{ route('events.show', $event) }}" class="event-link">Details ansehen </a>
<a href="{{ route('event.detail', $event) }}" class="event-link">Details ansehen </a>
</div>
</div>
@endforeach

View File

@ -0,0 +1,20 @@
@include('partials.header')
<main style="max-width:1100px;margin:40px auto;padding:0 20px;">
<h1 style="font-size:2rem;margin-bottom:16px;">Allgemeine Geschäftsbedingungen (AGB)</h1>
<section style="background:#fff;padding:20px;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
<h2 style="margin-top:0;">1. Geltungsbereich</h2>
<p>Diese AGB regeln die Nutzung der Plattform VeranstaltungsApp zwischen VeranstaltungsApp (Anbieter) und den Nutzern.</p>
<h2 style="margin-top:12px;">2. Leistungen</h2>
<p>Der Anbieter stellt eine Plattform zur Suche, Anzeige und Verwaltung von Veranstaltungen zur Verfügung.</p>
<h2 style="margin-top:12px;">3. Haftung</h2>
<p>Der Anbieter haftet nur bei Vorsatz und grober Fahrlässigkeit. Für Inhalte Dritter übernimmt der Anbieter keine Haftung.</p>
<p style="margin-top:20px;color:#6b7280;font-size:0.9em;">Dies ist eine MusterAGB. Bitte lassen Sie die AGB rechtlich prüfen, bevor Sie diese veröffentlichen.</p>
</section>
</main>
@include('partials.footer')

View File

@ -0,0 +1,23 @@
@include('partials.header')
<main style="max-width:1100px;margin:40px auto;padding:0 20px;">
<h1 style="font-size:2rem;margin-bottom:16px;">Impressum</h1>
<section style="background:#fff;padding:20px;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
<p><strong>Angaben gemäß § 5 TMG:</strong></p>
<p>VeranstaltungsApp<br>
Musterstraße 1<br>
12345 Musterstadt</p>
<p><strong>Kontakt:</strong><br>
Telefon: +49 (0)123 456789<br>
EMail: support@veranstaltungen.app</p>
<h2 style="margin-top:16px;">Vertretungsberechtigte(r)</h2>
<p>Max Mustermann</p>
<p style="margin-top:20px;color:#6b7280;font-size:0.9em;">Hinweis: Dieses Impressum ist ein Platzhalter. Prüfen Sie bitte die rechtlichen Anforderungen und ergänzen Sie ggf. Angaben wie UStID, Handelsregister etc.</p>
</section>
</main>
@include('partials.footer')

View File

@ -0,0 +1,25 @@
@include('partials.header')
<main style="max-width:1100px;margin:40px auto;padding:0 20px;">
<h1 style="font-size:2rem;margin-bottom:16px;">Datenschutzerklärung</h1>
<section style="background:#fff;padding:20px;border-radius:8px;box-shadow:0 1px 6px rgba(0,0,0,0.04);">
<p>Diese Datenschutzerklärung informiert Sie über die Verarbeitung personenbezogener Daten im Zusammenhang mit der Nutzung der VeranstaltungsApp.</p>
<h2 style="margin-top:16px;">1. Verantwortlicher</h2>
<p>Verantwortlich: VeranstaltungsApp, Musterstraße 1, 12345 Musterstadt, EMail: support@veranstaltungen.app</p>
<h2 style="margin-top:16px;">2. Erhobene Daten</h2>
<p>Wir verarbeiten u. a. Name, EMail, Nutzungsdaten und eventbezogene Informationen zur Bereitstellung der Dienste.</p>
<h2 style="margin-top:16px;">3. Zwecke & Rechtsgrundlagen</h2>
<p>Die Datenverarbeitung dient der Vertragserfüllung, Sicherheitszwecken und der Verbesserung unseres Dienstes. Rechtsgrundlagen sind Art.6 DSGVO.</p>
<h2 style="margin-top:16px;">4. Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Datenübertragbarkeit und Widerspruch. Kontaktieren Sie uns unter support@veranstaltungen.app.</p>
<p style="margin-top:20px;color:#6b7280;font-size:0.9em;">Diese Seite ist ein Musterdokument. Bitte passen Sie den Text an die rechtlichen Anforderungen Ihres Projekts an.</p>
</section>
</main>
@include('partials.footer')

View File

@ -1,46 +1,126 @@
<footer class="bg-gray-900 text-gray-300 mt-12">
<div class="max-w-7xl mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
<style>
.site-footer {
background: #1a1a2e;
color: #9ca3af;
margin-top: 80px;
font-size: 0.875em;
}
.site-footer .inner {
max-width: 1200px;
margin: 0 auto;
padding: 56px 20px 32px;
}
.site-footer .grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 48px;
margin-bottom: 48px;
}
@media (max-width: 768px) {
.site-footer .grid { grid-template-columns: 1fr 1fr; gap: 32px; }
}
.site-footer .brand-name {
font-size: 1.1em;
font-weight: 800;
color: white;
letter-spacing: -0.02em;
margin-bottom: 10px;
}
.site-footer .brand-name span { color: #818cf8; }
.site-footer .brand-desc {
line-height: 1.65;
color: #6b7280;
}
.site-footer h5 {
color: white;
font-weight: 700;
margin-bottom: 16px;
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.site-footer ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.site-footer ul a {
color: #9ca3af;
text-decoration: none;
transition: color 0.2s;
}
.site-footer ul a:hover { color: white; }
.site-footer .divider {
border: none;
border-top: 1px solid #2d2d44;
margin-bottom: 28px;
}
.site-footer .bottom {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.site-footer .copyright { color: #6b7280; }
.site-footer .social {
display: flex;
gap: 20px;
}
.site-footer .social a {
color: #6b7280;
text-decoration: none;
transition: color 0.2s;
}
.site-footer .social a:hover { color: white; }
</style>
<footer class="site-footer">
<div class="inner">
<div class="grid">
<div>
<h4 class="text-white font-bold mb-4">Veranstaltungs-App</h4>
<p class="text-sm">Die beste Plattform zur Entdeckung und Verwaltung von Veranstaltungen in Deiner Nähe.</p>
<div class="brand-name">Veranstaltungs<span>-App</span></div>
<p class="brand-desc">Die beste Plattform zur Entdeckung und Verwaltung von Veranstaltungen in Deiner Nähe.</p>
</div>
<div>
<h4 class="text-white font-bold mb-4">Navigation</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ url('/') }}" class="hover:text-white transition">Home</a></li>
<li><a href="{{ route('events') }}" class="hover:text-white transition">Events</a></li>
<h5>Navigation</h5>
<ul>
<li><a href="{{ url('/') }}">Home</a></li>
<li><a href="{{ route('events') }}">Events</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-bold mb-4">Rechtliches</h4>
<ul class="space-y-2 text-sm">
<li><a href="#" class="hover:text-white transition">Datenschutzerklärung</a></li>
<li><a href="#" class="hover:text-white transition">Impressum</a></li>
<li><a href="#" class="hover:text-white transition">AGB</a></li>
<h5>Rechtliches</h5>
<ul>
<li><a href="{{ route('privacy') }}">Datenschutz</a></li>
<li><a href="{{ route('impressum') }}">Impressum</a></li>
<li><a href="{{ route('agb') }}">AGB</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-bold mb-4">Kontakt</h4>
<ul class="space-y-2 text-sm">
<li>Email: <a href="mailto:support@veranstaltungen.app" class="text-blue-400 hover:text-blue-300">support@veranstaltungen.app</a></li>
<li>Telefon: +49 (0)123 456789</li>
<li>Adresse: Musterstraße 1, 12345 Musterstadt</li>
<h5>Kontakt</h5>
<ul>
<li><a href="mailto:support@veranstaltungen.app">support@veranstaltungen.app</a></li>
<li>+49 (0)123 456789</li>
<li>Musterstraße 1, 12345 Musterstadt</li>
</ul>
</div>
</div>
<div class="border-t border-gray-700 my-8"></div>
<hr class="divider">
<div class="flex flex-col md:flex-row items-center justify-between">
<div class="text-sm text-gray-400 mb-4 md:mb-0">&copy; {{ date('Y') }} Veranstaltungs-App. Alle Rechte vorbehalten.</div>
<div class="flex gap-6">
<a href="https://facebook.com" class="text-gray-400 hover:text-white transition" target="_blank">Facebook</a>
<a href="https://twitter.com" class="text-gray-400 hover:text-white transition" target="_blank">Twitter</a>
<a href="https://instagram.com" class="text-gray-400 hover:text-white transition" target="_blank">Instagram</a>
<div class="bottom">
<span class="copyright">&copy; {{ date('Y') }} Veranstaltungs-App. Alle Rechte vorbehalten.</span>
<div class="social">
<a href="https://facebook.com" target="_blank">Facebook</a>
<a href="https://twitter.com" target="_blank">Twitter</a>
<a href="https://instagram.com" target="_blank">Instagram</a>
</div>
</div>
</div>

View File

@ -1,33 +1,162 @@
<header class="bg-white dark:bg-gray-800 shadow-md mb-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-3">
<a href="{{ url('/') }}" class="flex items-center gap-3">
<span class="text-xl font-bold text-gray-900 dark:text-white">Veranstaltungs-App</span>
</a>
</div>
<style>
.site-header {
background: white;
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
}
.site-header .inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
}
.site-header .logo {
font-size: 1.2em;
font-weight: 800;
color: #1a1a2e;
text-decoration: none;
letter-spacing: -0.02em;
}
.site-header .logo span { color: #667eea; }
.site-header nav {
display: flex;
align-items: center;
gap: 28px;
}
.site-header nav a {
color: #6b7280;
text-decoration: none;
font-size: 0.9em;
font-weight: 500;
transition: color 0.2s;
}
.site-header nav a:hover { color: #667eea; }
<nav class="hidden md:flex items-center gap-6">
<a href="{{ url('/') }}" class="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition">Home</a>
<a href="{{ route('events') }}" class="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition">Events</a>
<a href="{{ url('/api/events') }}" class="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition">API</a>
</nav>
/* Auth area */
.site-header .auth-area { display: flex; align-items: center; gap: 10px; }
.site-header .btn-outline {
padding: 7px 16px; border: 1.5px solid #d1d5db; border-radius: 8px;
font-size: 0.875em; font-weight: 500; color: #374151; text-decoration: none;
background: white; transition: border-color 0.2s, color 0.2s;
}
.site-header .btn-outline:hover { border-color: #667eea; color: #667eea; }
.site-header .btn-primary {
padding: 7px 16px; background: linear-gradient(135deg,#667eea 0%,#764ba2 100%);
border-radius: 8px; font-size: 0.875em; font-weight: 600; color: white;
text-decoration: none; border: none; cursor: pointer; transition: opacity 0.2s;
}
.site-header .btn-primary:hover { opacity: 0.9; }
<div class="flex items-center gap-4">
@auth
<div class="flex items-center gap-3">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ Auth::user()->name }}</span>
<a href="{{ route('profile') }}" class="inline-block px-3 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition text-sm">Profil</a>
<form method="POST" action="{{ route('logout') }}" class="inline">
/* Avatar dropdown */
.avatar-wrapper { position: relative; }
.avatar-btn {
width: 38px; height: 38px; border-radius: 50%;
background: linear-gradient(135deg,#667eea 0%,#764ba2 100%);
border: none; cursor: pointer; display: flex; align-items: center;
justify-content: center; color: white; font-weight: 700; font-size: 0.95em;
letter-spacing: 0.02em; transition: box-shadow 0.2s; flex-shrink: 0;
}
.avatar-btn:hover { box-shadow: 0 0 0 3px rgba(102,126,234,0.25); }
.avatar-dropdown {
display: none;
position: absolute; right: 0; top: calc(100% + 10px);
background: white; border: 1px solid #e5e7eb; border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12); min-width: 220px; overflow: hidden;
z-index: 200;
}
.avatar-dropdown.open { display: block; }
.avatar-dropdown .dd-header {
padding: 14px 16px 12px; border-bottom: 1px solid #f3f4f6;
}
.avatar-dropdown .dd-name { font-weight: 700; font-size: 0.95em; color: #1a1a2e; }
.avatar-dropdown .dd-email { font-size: 0.78em; color: #9ca3af; margin-top: 2px; }
.avatar-dropdown .dd-link {
display: flex; align-items: center; gap: 10px;
padding: 11px 16px; font-size: 0.875em; color: #374151;
text-decoration: none; transition: background 0.15s;
}
.avatar-dropdown .dd-link:hover { background: #f9fafb; color: #667eea; }
.avatar-dropdown .dd-link svg { width: 16px; height: 16px; flex-shrink: 0; }
.avatar-dropdown .dd-divider { border: none; border-top: 1px solid #f3f4f6; margin: 0; }
.avatar-dropdown .dd-logout-form { margin: 0; }
.avatar-dropdown .dd-logout-btn {
width: 100%; display: flex; align-items: center; gap: 10px;
padding: 11px 16px; font-size: 0.875em; color: #dc2626;
background: none; border: none; cursor: pointer; transition: background 0.15s;
text-align: left;
}
.avatar-dropdown .dd-logout-btn:hover { background: #fef2f2; }
.avatar-dropdown .dd-logout-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
</style>
<header class="site-header">
<div class="inner">
<a href="{{ url('/') }}" class="logo">Veranstaltungs<span>-App</span></a>
<nav>
<a href="{{ url('/') }}">Home</a>
<a href="{{ route('events') }}">Events</a>
</nav>
<div class="auth-area">
@auth
{{-- Avatar mit Dropdown --}}
<div class="avatar-wrapper" id="avatarWrapper">
<button class="avatar-btn" id="avatarBtn" title="{{ Auth::user()->name }}">
{{ strtoupper(substr(Auth::user()->name, 0, 1)) }}
</button>
<div class="avatar-dropdown" id="avatarDropdown">
<div class="dd-header">
<div class="dd-name">{{ Auth::user()->name }}</div>
<div class="dd-email">{{ Auth::user()->email }}</div>
</div>
<a href="{{ route('profile') }}" class="dd-link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
Mein Profil
</a>
<hr class="dd-divider">
<form method="POST" action="{{ route('logout') }}" class="dd-logout-form">
@csrf
<button type="submit" class="inline-block px-3 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition text-sm">Logout</button>
<button type="submit" class="dd-logout-btn">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
Abmelden
</button>
</form>
</div>
@else
<a href="{{ route('login') }}" class="inline-block px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition text-sm font-medium">Anmelden</a>
<a href="{{ route('register') }}" class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition text-sm font-medium">Registrieren</a>
@endauth
</div>
</div>
@else
<a href="{{ route('login') }}" class="btn-outline">Anmelden</a>
<a href="{{ route('register') }}" class="btn-primary">Registrieren</a>
@endauth
</div>
</div>
</header>
<script>
(function () {
var btn = document.getElementById('avatarBtn');
var dropdown = document.getElementById('avatarDropdown');
if (!btn || !dropdown) return;
btn.addEventListener('click', function (e) {
e.stopPropagation();
dropdown.classList.toggle('open');
});
document.addEventListener('click', function (e) {
if (!dropdown.contains(e.target)) {
dropdown.classList.remove('open');
}
});
})();
</script>

View File

@ -3,116 +3,163 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Profil</title>
<script src="https://cdn.tailwindcss.com"></script>
<title>Mein Profil Veranstaltungs-App</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9f9f9; color: #1a1a2e; }
main { max-width: 760px; margin: 40px auto; padding: 0 20px 80px; }
.page-title { font-size: 1.8em; font-weight: 800; margin-bottom: 28px; letter-spacing: -0.02em; }
/* Alert boxes */
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 0.9em; font-weight: 500; }
.alert-success { background: #ecfdf5; color: #065f46; border: 1px solid #6ee7b7; }
.alert-error { background: #fef2f2; color: #991b1b; border: 1px solid #fca5a5; }
/* Cards */
.card { background: white; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 24px; overflow: hidden; }
.card-header { padding: 18px 24px; border-bottom: 1px solid #f3f4f6; }
.card-header h2 { font-size: 1.05em; font-weight: 700; }
.card-header p { font-size: 0.85em; color: #6b7280; margin-top: 3px; }
.card-body { padding: 24px; }
/* Info-grid */
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media(max-width:560px){ .info-grid { grid-template-columns: 1fr; } }
.info-item label { display: block; font-size: 0.78em; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }
.info-item .value { font-size: 1em; font-weight: 500; }
.badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 0.78em; font-weight: 700; background: #eff6ff; color: #1d4ed8; }
.badge.admin { background: #fef3c7; color: #92400e; }
/* Forms */
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 0.875em; font-weight: 600; margin-bottom: 6px; }
.form-group input {
width: 100%; padding: 10px 12px; border: 1.5px solid #d1d5db;
border-radius: 8px; font-size: 0.95em; transition: border-color 0.2s;
background: white;
}
.form-group input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,0.12); }
.form-group .hint { font-size: 0.78em; color: #9ca3af; margin-top: 4px; }
.error-msg { font-size: 0.8em; color: #dc2626; margin-top: 4px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media(max-width:560px){ .form-row { grid-template-columns: 1fr; } }
/* Buttons */
.btn-primary {
padding: 10px 22px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 8px; font-size: 0.9em;
font-weight: 600; cursor: pointer; transition: opacity 0.2s; width: 100%;
}
.btn-primary:hover { opacity: 0.9; }
.divider { border: none; border-top: 1px solid #f3f4f6; margin: 20px 0; }
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900">
<div class="min-h-screen">
<!-- Header -->
<header class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-4xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Veranstaltungs-App</h1>
<div class="flex gap-2">
<a href="{{ route('events') }}" class="text-blue-600 hover:text-blue-800">Events</a>
<form method="POST" action="{{ route('logout') }}" class="inline">
@csrf
<button type="submit" class="text-blue-600 hover:text-blue-800">Logout</button>
</form>
<body>
@include('partials.header')
<main>
<h1 class="page-title">Mein Profil</h1>
{{-- Success / Error messages --}}
@if(session('success_info'))
<div class="alert alert-success"> {{ session('success_info') }}</div>
@endif
@if(session('success_pw'))
<div class="alert alert-success"> {{ session('success_pw') }}</div>
@endif
{{-- ── Profil-Übersicht ── --}}
<div class="card">
<div class="card-header">
<h2>Profilübersicht</h2>
<p>Deine aktuellen Kontodaten auf einen Blick</p>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<label>Name</label>
<span class="value">{{ $user->name }}</span>
</div>
<div class="info-item">
<label>EMail</label>
<span class="value">{{ $user->email }}</span>
</div>
<div class="info-item">
<label>Rolle</label>
<span class="badge {{ $user->role === 'admin' ? 'admin' : '' }}">
{{ $user->role === 'admin' ? 'Administrator' : ($user->role === 'organizer' ? 'Veranstalter' : 'Nutzer') }}
</span>
</div>
<div class="info-item">
<label>Mitglied seit</label>
<span class="value">{{ $user->created_at->format('d.m.Y') }}</span>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 gap-6">
<!-- Profile Card -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Mein Profil</h2>
</div>
<div class="px-6 py-6">
<dl class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-lg text-gray-900 dark:text-white">{{ Auth::user()->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">E-Mail</dt>
<dd class="mt-1 text-lg text-gray-900 dark:text-white">{{ Auth::user()->email }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Rolle</dt>
<dd class="mt-1">
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
@if(Auth::user()->role === 'admin')
Administrator
@elseif(Auth::user()->role === 'organizer')
Veranstalter
@else
Benutzer
@endif
</span>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Registriert am</dt>
<dd class="mt-1 text-lg text-gray-900 dark:text-white">
{{ Auth::user()->created_at->format('d.m.Y H:i') }}
</dd>
</div>
</dl>
</div>
</div>
<!-- API Endpoints Card -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Verfügbare API Endpoints</h2>
</div>
<div class="px-6 py-6">
<div class="space-y-4">
<div class="border-l-4 border-blue-500 pl-4">
<p class="font-mono text-sm text-gray-900 dark:text-white">GET /api/user/profile</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Dein Profil abrufen</p>
</div>
<div class="border-l-4 border-blue-500 pl-4">
<p class="font-mono text-sm text-gray-900 dark:text-white">GET /api/user/events</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Deine erstellten Events</p>
</div>
<div class="border-l-4 border-blue-500 pl-4">
<p class="font-mono text-sm text-gray-900 dark:text-white">GET /api/user/favorites</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Deine Favoriten</p>
</div>
<div class="border-l-4 border-blue-500 pl-4">
<p class="font-mono text-sm text-gray-900 dark:text-white">POST /api/user/favorites/{id}/toggle</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Event zu Favoriten hinzufügen/entfernen</p>
</div>
<div class="border-l-4 border-blue-500 pl-4">
<p class="font-mono text-sm text-gray-900 dark:text-white">GET /api/user/stats</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Deine Statistiken</p>
</div>
</div>
</div>
</div>
<!-- Quick Links Card -->
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Schnelllinks</h2>
</div>
<div class="px-6 py-6">
<div class="flex gap-4 flex-wrap">
<a href="{{ route('events') }}" class="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Zu Events
</a>
<a href="{{ route('forgot-password') }}" class="inline-block px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded hover:bg-gray-300">
Passwort ändern
</a>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
{{-- ── Name & E-Mail ändern ── --}}
<div class="card">
<div class="card-header">
<h2>Name & EMail ändern</h2>
<p>Passe deinen Anzeigenamen und deine EMail-Adresse an</p>
</div>
<div class="card-body">
@if($errors->has('name') || $errors->has('email'))
<div class="alert alert-error">Bitte korrigiere die markierten Felder.</div>
@endif
<form method="POST" action="{{ route('profile.update.info') }}">
@csrf @method('PUT')
<div class="form-row">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" value="{{ old('name', $user->name) }}" required>
@error('name')<span class="error-msg">{{ $message }}</span>@enderror
</div>
<div class="form-group">
<label for="email">EMail</label>
<input type="email" id="email" name="email" value="{{ old('email', $user->email) }}" required>
@error('email')<span class="error-msg">{{ $message }}</span>@enderror
</div>
</div>
<button type="submit" class="btn-primary">Änderungen speichern</button>
</form>
</div>
</div>
{{-- ── Passwort ändern ── --}}
<div class="card">
<div class="card-header">
<h2>Passwort ändern</h2>
<p>Wähle ein sicheres Passwort mit mindestens 8 Zeichen</p>
</div>
<div class="card-body">
@if($errors->has('current_password') || $errors->has('password'))
<div class="alert alert-error">Bitte korrigiere die markierten Felder.</div>
@endif
<form method="POST" action="{{ route('profile.update.password') }}">
@csrf @method('PUT')
<div class="form-group">
<label for="current_password">Aktuelles Passwort</label>
<input type="password" id="current_password" name="current_password" required autocomplete="current-password">
@error('current_password')<span class="error-msg">{{ $message }}</span>@enderror
</div>
<div class="form-row">
<div class="form-group">
<label for="password">Neues Passwort</label>
<input type="password" id="password" name="password" required autocomplete="new-password">
<span class="hint">Mindestens 8 Zeichen</span>
@error('password')<span class="error-msg">{{ $message }}</span>@enderror
</div>
<div class="form-group">
<label for="password_confirmation">Neues Passwort bestätigen</label>
<input type="password" id="password_confirmation" name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<button type="submit" class="btn-primary">Passwort ändern</button>
</form>
</div>
</div>
</main>
@include('partials.footer')
</body>
</html>

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\EventWebController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
@ -9,16 +10,22 @@ Route::get('/', [EventWebController::class, 'index'])->name('home');
// Event Routes
Route::get('/events', [EventWebController::class, 'index'])->name('events');
Route::get('/events/{event}', [EventWebController::class, 'show'])->name('events.show');
Route::get('/events/{event}', [EventWebController::class, 'show'])->name('event.detail');
// Auth Routes
Route::get('/login', function () {
return view('auth.login');
})->name('login');
})->name('login')->middleware('guest');
Route::post('/login', [\App\Http\Controllers\AuthController::class, 'webLogin'])->name('login.post')->middleware('guest');
Route::get('/register', function () {
return view('auth.register');
})->name('register');
})->name('register')->middleware('guest');
Route::post('/register', [\App\Http\Controllers\AuthController::class, 'webRegister'])->name('register.post')->middleware('guest');
Route::post('/logout', [\App\Http\Controllers\AuthController::class, 'webLogout'])->name('logout');
Route::get('/forgot-password', function () {
return view('auth.forgot-password');
@ -29,13 +36,16 @@ Route::get('/reset-password', function () {
})->name('reset-password');
// Protected Routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/profile', function () {
return view('profile');
})->name('profile');
Route::post('/logout', [\App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
Route::put('/profile/info', [ProfileController::class, 'updateInfo'])->name('profile.update.info');
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.update.password');
});
// Swagger API Documentation Routes
require __DIR__ . '/swagger.php';
// Legal pages
Route::view('/datenschutz', 'legal.privacy')->name('privacy');
Route::view('/impressum', 'legal.impressum')->name('impressum');
Route::view('/agb', 'legal.agb')->name('agb');