diff --git a/README.md b/README.md index a8346e1..05debb3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ php -S 127.0.0.1:8000 Danach ist die Seite unter `http://127.0.0.1:8000` erreichbar. +Wenn die Anwendung unter einem Unterordner laeuft, kannst du zusaetzlich `FOTOBOX_BASE_PATH` setzen, z. B.: + +```bash +FOTOBOX_BASE_PATH=/fotobox php -S 127.0.0.1:8000 +``` + ## Admin-Zugang - Benutzername: `admin` diff --git a/assets/styles.css b/assets/styles.css index a495026..8e684e4 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -1,19 +1,20 @@ :root { - --bg: #f4efe7; - --bg-panel: rgba(255, 255, 255, 0.72); - --card: #fdf9f2; + --bg: #f4f0e8; + --bg-panel: rgba(255, 252, 247, 0.84); + --card: #fffdf8; --surface: #ffffff; - --line: rgba(55, 35, 18, 0.12); - --text: #1f160f; - --muted: #6d5a4d; - --accent: #bd5f2d; - --accent-deep: #7a3412; - --accent-soft: #f1d1ba; + --line: rgba(30, 24, 19, 0.1); + --text: #171412; + --muted: #5a5048; + --accent: #b56a38; + --accent-deep: #6e3413; + --accent-soft: #edd8c9; + --trust: #2f5a4d; --success: #1f6c46; --error: #8a2630; - --shadow: 0 24px 60px rgba(57, 35, 18, 0.14); - --radius-lg: 28px; - --radius-md: 18px; + --shadow: 0 24px 60px rgba(33, 24, 17, 0.1); + --radius-lg: 32px; + --radius-md: 20px; --radius-sm: 12px; } @@ -28,12 +29,12 @@ html { body { margin: 0; min-height: 100vh; - font-family: "Avenir Next", "Segoe UI", sans-serif; + font-family: "Trebuchet MS", "Gill Sans", sans-serif; color: var(--text); background: - radial-gradient(circle at top left, rgba(203, 135, 77, 0.25), transparent 28%), - radial-gradient(circle at 85% 15%, rgba(122, 52, 18, 0.12), transparent 22%), - linear-gradient(180deg, #faf4eb 0%, var(--bg) 100%); + radial-gradient(circle at top left, rgba(214, 171, 135, 0.2), transparent 28%), + radial-gradient(circle at 90% 12%, rgba(59, 95, 82, 0.08), transparent 26%), + linear-gradient(180deg, #fbf8f2 0%, var(--bg) 100%); } a { @@ -64,8 +65,8 @@ textarea { top: 18px; z-index: 20; margin-bottom: 32px; - background: rgba(252, 248, 241, 0.82); - border: 1px solid rgba(255, 255, 255, 0.7); + background: rgba(252, 248, 241, 0.88); + border: 1px solid rgba(255, 255, 255, 0.75); border-radius: 999px; backdrop-filter: blur(16px); box-shadow: 0 12px 30px rgba(58, 39, 24, 0.08); @@ -75,14 +76,14 @@ textarea { display: inline-flex; align-items: center; gap: 14px; - font-family: Georgia, "Times New Roman", serif; + font-family: "Baskerville", "Iowan Old Style", Georgia, serif; } .brand strong, h1, h2, h3 { - font-family: Georgia, "Times New Roman", serif; + font-family: "Baskerville", "Iowan Old Style", Georgia, serif; letter-spacing: -0.02em; } @@ -90,7 +91,7 @@ h3 { display: block; color: var(--muted); font-size: 0.8rem; - font-family: "Avenir Next", "Segoe UI", sans-serif; + font-family: "Trebuchet MS", "Gill Sans", sans-serif; } .brand-mark { @@ -140,7 +141,7 @@ h3 { } .button-secondary { - background: rgba(255, 255, 255, 0.72); + background: rgba(255, 255, 255, 0.78); color: var(--text); border: 1px solid var(--line); } @@ -167,8 +168,10 @@ main { } .hero-section, +.trust-bar, .feature-strip, .content-grid, +.occasion-section, .pricing-panel, .availability-section, .booking-section, @@ -183,9 +186,9 @@ main { .hero-section { display: grid; - grid-template-columns: 1.2fr 0.9fr; - gap: 28px; - padding: 42px; + grid-template-columns: 1.08fr 0.92fr; + gap: 34px; + padding: 48px; } .eyebrow { @@ -201,8 +204,8 @@ main { .admin-login-card h1, .section-header h1 { margin: 0; - font-size: clamp(2.6rem, 5vw, 4.8rem); - line-height: 0.96; + font-size: clamp(2.8rem, 5vw, 5rem); + line-height: 0.94; } .hero-text, @@ -221,25 +224,59 @@ main { margin: 28px 0 26px; } -.hero-points, -.check-list, -.compact-list { - margin: 0; - padding-left: 20px; - color: var(--muted); +.hero-highlight-grid { display: grid; - gap: 10px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.hero-highlight-card, +.feature-strip article, +.content-block, +.pricing-aside, +.booking-form-card, +.table-card, +.admin-login-card, +.booking-info-card, +.availability-card, +.trust-bar article, +.occasion-grid article, +.form-section { + background: rgba(255, 255, 255, 0.76); + border: 1px solid var(--line); + border-radius: var(--radius-md); +} + +.hero-highlight-card { + padding: 16px 18px; +} + +.hero-highlight-card span { + display: block; + color: var(--trust); + font-size: 0.76rem; + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 8px; + font-weight: 700; +} + +.hero-highlight-card strong { + display: block; + font-size: 1rem; + line-height: 1.35; } .hero-card { display: grid; gap: 16px; - padding: 18px; + padding: 22px; background: - linear-gradient(145deg, rgba(255, 244, 232, 0.92), rgba(246, 225, 209, 0.85)), + linear-gradient(145deg, rgba(255, 251, 246, 0.95), rgba(246, 233, 220, 0.9)), var(--card); border-radius: 24px; min-height: 520px; + border: 1px solid rgba(104, 65, 39, 0.12); } .hero-card-panel { @@ -248,7 +285,7 @@ main { gap: 16px; padding: 16px 18px; border-radius: 18px; - background: rgba(255, 255, 255, 0.55); + background: rgba(255, 255, 255, 0.58); color: var(--muted); } @@ -262,56 +299,142 @@ main { place-items: center; overflow: hidden; border-radius: 24px; - min-height: 320px; - background: linear-gradient(180deg, rgba(118, 59, 31, 0.15), rgba(33, 18, 11, 0.24)); + min-height: 340px; + background: + radial-gradient(circle at 50% 12%, rgba(255, 243, 229, 0.95), transparent 28%), + linear-gradient(180deg, rgba(212, 189, 170, 0.42), rgba(57, 43, 35, 0.08)); } -.camera-glow { +.device-plinth { position: absolute; - width: 240px; - height: 240px; - border-radius: 50%; - background: radial-gradient(circle, rgba(255, 228, 195, 0.9) 0%, rgba(241, 179, 119, 0.15) 58%, transparent 72%); + bottom: 32px; + width: 220px; + height: 24px; + border-radius: 999px; + background: rgba(52, 38, 30, 0.12); + filter: blur(6px); } -.camera-body { +.camera-tower { position: relative; - width: 280px; - height: 200px; + display: grid; + justify-items: center; + z-index: 1; +} + +.camera-head { + position: relative; + width: 210px; + height: 230px; border-radius: 30px; - background: linear-gradient(180deg, #2a2320 0%, #13100f 100%); + background: linear-gradient(180deg, #25201c 0%, #12100e 100%); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 18px 42px rgba(0, 0, 0, 0.35); } -.camera-body::before { +.camera-head::before { content: ""; position: absolute; - inset: -22px 40px auto auto; - width: 90px; - height: 70px; - border-radius: 24px 24px 8px 8px; + inset: 18px 18px auto auto; + width: 58px; + height: 90px; + border-radius: 18px; + background: linear-gradient(180deg, rgba(239, 179, 137, 0.9), rgba(255, 252, 248, 0.18)); +} + +.camera-head::after { + content: ""; + position: absolute; + inset: -20px auto auto 34px; + width: 72px; + height: 52px; + border-radius: 18px 18px 8px 8px; background: linear-gradient(180deg, #28211e 0%, #141111 100%); } .camera-lens { position: absolute; - inset: 34px auto auto 78px; - width: 124px; - height: 124px; + inset: 54px auto auto 38px; + width: 132px; + height: 132px; border-radius: 50%; background: radial-gradient(circle at 42% 42%, rgba(95, 170, 221, 0.95) 0%, rgba(41, 86, 111, 0.9) 22%, rgba(4, 7, 10, 0.95) 58%, #050607 100%); border: 12px solid #4a403a; } -.camera-screen { +.camera-flash { position: absolute; - inset: 48px 28px auto auto; - width: 56px; - height: 92px; - border-radius: 16px; - background: linear-gradient(180deg, rgba(255, 188, 125, 0.85), rgba(255, 240, 224, 0.22)); - box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12); + inset: 32px 28px auto auto; + width: 26px; + height: 26px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 251, 245, 1) 0%, rgba(246, 197, 151, 0.78) 58%, rgba(255, 255, 255, 0.15) 100%); +} + +.camera-stand { + width: 18px; + height: 108px; + background: linear-gradient(180deg, #161311 0%, #3c342f 100%); +} + +.camera-base { + width: 160px; + height: 14px; + border-radius: 999px; + background: #241d18; +} + +.photo-strip { + position: absolute; + top: 46px; + display: grid; + gap: 10px; + padding: 14px 10px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(31, 21, 14, 0.08); + color: var(--trust); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.76rem; +} + +.photo-strip-left { + left: 28px; + transform: rotate(-8deg); +} + +.photo-strip-right { + right: 28px; + top: 80px; + transform: rotate(7deg); +} + +.trust-bar { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; + padding: 18px; +} + +.trust-bar article { + padding: 18px 20px; +} + +.trust-bar span { + display: block; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--trust); + margin-bottom: 10px; + font-weight: 700; +} + +.trust-bar strong { + display: block; + line-height: 1.35; } .feature-strip { @@ -323,19 +446,9 @@ main { .feature-strip article, .content-block, -.pricing-aside, -.booking-form-card, .table-card, -.admin-login-card, -.booking-info-card, -.availability-card { - background: rgba(255, 255, 255, 0.7); - border: 1px solid var(--line); - border-radius: var(--radius-md); -} - -.feature-strip article { - padding: 24px; +.admin-login-card { + padding: 28px; } .feature-strip h2, @@ -343,7 +456,8 @@ main { .pricing-panel h2, .booking-copy h2, .faq-section h2, -.table-card h2 { +.table-card h2, +.section-heading h2 { margin: 0 0 12px; font-size: clamp(1.6rem, 3vw, 2.3rem); } @@ -360,7 +474,8 @@ main { .stack-form span, .stack-form label, .booking-form span, -.form-note { +.form-note, +.occasion-grid span { color: var(--muted); line-height: 1.6; } @@ -374,12 +489,6 @@ main { padding: 24px; } -.content-block, -.table-card, -.admin-login-card { - padding: 28px; -} - .step-list { display: grid; gap: 18px; @@ -391,14 +500,41 @@ main { .step-list li { display: grid; gap: 6px; - padding: 16px 18px; - border-radius: var(--radius-sm); - background: rgba(246, 232, 221, 0.72); + padding: 18px 20px; + border-radius: 18px; + background: rgba(246, 238, 229, 0.72); + border-left: 4px solid var(--accent); +} + +.step-list strong { + display: flex; + align-items: center; + gap: 12px; +} + +.step-number { + display: inline-grid; + place-items: center; + width: 30px; + height: 30px; + border-radius: 50%; + background: var(--text); + color: #fff; + font-size: 0.85rem; +} + +.check-list, +.compact-list { + margin: 0; + padding-left: 20px; + display: grid; + gap: 10px; } .pricing-panel, .availability-section, -.faq-section { +.faq-section, +.occasion-section { padding: 28px; } @@ -420,6 +556,22 @@ main { font-size: 1.45rem; } +.section-heading { + margin-bottom: 18px; +} + +.occasion-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.occasion-grid article { + padding: 22px; + display: grid; + gap: 8px; +} + .availability-list, .faq-list, .stats-grid { @@ -438,6 +590,11 @@ main { gap: 8px; } +.availability-card small { + color: var(--trust); + font-weight: 700; +} + .availability-card-empty { grid-column: 1 / -1; } @@ -456,16 +613,45 @@ main { padding: 28px; } +.booking-form, +.stack-form { + display: grid; + gap: 18px; +} + .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } -.booking-form, -.stack-form { - display: grid; - gap: 18px; +.form-grid-two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.form-section { + padding: 18px; +} + +.form-section-header { + margin-bottom: 14px; +} + +.form-section-header h3 { + margin: 6px 0 0; + font-size: 1.3rem; +} + +.form-step { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--trust); + font-weight: 700; +} + +.field-full { + grid-column: 1 / -1; } label { @@ -491,11 +677,16 @@ textarea { .price-summary { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; padding: 18px; border-radius: 18px; - background: rgba(245, 223, 202, 0.55); + background: linear-gradient(135deg, rgba(244, 230, 217, 0.78), rgba(220, 234, 228, 0.72)); +} + +.price-summary-total { + border-left: 1px solid rgba(31, 21, 14, 0.12); + padding-left: 16px; } .flash { @@ -627,6 +818,10 @@ th { margin: 0; } +.theme-admin .site-header { + background: rgba(250, 248, 244, 0.92); +} + @media (max-width: 1080px) { .hero-section, .content-grid, @@ -637,6 +832,11 @@ th { grid-template-columns: 1fr; } + .trust-bar, + .occasion-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .stats-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -659,6 +859,9 @@ th { .feature-strip, .faq-list, .form-grid, + .hero-highlight-grid, + .trust-bar, + .occasion-grid, .price-summary { grid-template-columns: 1fr; } @@ -674,7 +877,8 @@ th { .availability-section, .booking-section, .faq-section, - .admin-section { + .admin-section, + .occasion-section { padding: 20px; } @@ -683,6 +887,11 @@ th { .section-header h1 { font-size: 2.35rem; } + + .price-summary-total { + border-left: 0; + padding-left: 0; + } } @media (max-width: 520px) { diff --git a/config.php b/config.php index 29707c1..59be274 100644 --- a/config.php +++ b/config.php @@ -3,6 +3,9 @@ declare(strict_types=1); return [ + 'app' => [ + 'base_path' => getenv('FOTOBOX_BASE_PATH') ?: '', + ], 'company' => [ 'name' => 'Fotobox Moments', 'tagline' => 'Fotobox-Vermietung fuer Hochzeiten, Geburtstage und Firmenevents', diff --git a/src/Support/functions.php b/src/Support/functions.php index ebdf9be..40deca6 100644 --- a/src/Support/functions.php +++ b/src/Support/functions.php @@ -2,6 +2,16 @@ declare(strict_types=1); +function setAppConfig(array $config): void +{ + $GLOBALS['app_config'] = $config; +} + +function appConfig(): array +{ + return $GLOBALS['app_config'] ?? []; +} + function render(string $view, array $data = []): void { extract($data, EXTR_SKIP); @@ -9,14 +19,75 @@ function render(string $view, array $data = []): void require dirname(__DIR__, 1) . '/../views/layout.php'; } +function basePath(): string +{ + $configured = trim((string) (appConfig()['app']['base_path'] ?? '')); + if ($configured !== '' && $configured !== '/') { + return '/' . trim($configured, '/'); + } + + $forwardedPrefix = trim((string) ($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '')); + if ($forwardedPrefix !== '' && $forwardedPrefix !== '/') { + return '/' . trim($forwardedPrefix, '/'); + } + + $requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; + + $knownSuffixes = [ + '/admin/order/invoice', + '/admin/order/update', + '/admin/invoice/pdf', + '/admin/create', + '/admin/order', + '/admin/login', + '/admin/logout', + '/admin', + '/book', + '/assets/styles.css', + '/assets/app.js', + '/', + ]; + + foreach ($knownSuffixes as $suffix) { + if ($suffix === '/') { + if ($requestPath !== '/' && str_ends_with($requestPath, '/')) { + return rtrim($requestPath, '/'); + } + + continue; + } + + if ($requestPath === $suffix) { + return ''; + } + + if (str_ends_with($requestPath, $suffix)) { + $prefix = substr($requestPath, 0, -strlen($suffix)); + return $prefix === false ? '' : rtrim($prefix, '/'); + } + } + + return ''; +} + +function url(string $path = ''): string +{ + $base = basePath(); + if ($path === '' || $path === '/') { + return $base === '' ? '/' : $base; + } + + return ($base === '' ? '' : $base) . '/' . ltrim($path, '/'); +} + function asset(string $path): string { - return '/' . ltrim($path, '/'); + return url($path); } function redirect(string $path): void { - header('Location: ' . $path); + header('Location: ' . url($path)); exit; } diff --git a/src/bootstrap.php b/src/bootstrap.php index b71bb9f..d63e540 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -14,6 +14,7 @@ session_start(); function runApplication(): void { $config = require dirname(__DIR__) . '/config.php'; + setAppConfig($config); [$bookingRepository, $invoiceRepository] = resolveRepositories($config); @@ -21,7 +22,11 @@ function runApplication(): void $invoicePdfService = new InvoicePdfService($config); $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; - $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; + $path = requestPath(); + + if (serveStaticAssetIfRequested($path)) { + return; + } if ($method === 'POST' && $path === '/book') { handlePublicBooking($bookingService); @@ -52,6 +57,53 @@ function runApplication(): void renderHome($bookingService, $config); } +function requestPath(): string +{ + $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; + $basePath = basePath(); + + if ($basePath !== '' && str_starts_with($path, $basePath)) { + $path = substr($path, strlen($basePath)) ?: '/'; + } + + if ($path === '') { + return '/'; + } + + return '/' . ltrim($path, '/'); +} + +function serveStaticAssetIfRequested(string $path): bool +{ + if (!str_starts_with($path, '/assets/')) { + return false; + } + + $file = dirname(__DIR__) . $path; + if (!is_file($file)) { + http_response_code(404); + echo 'Datei nicht gefunden.'; + return true; + } + + $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + $mimeTypes = [ + 'css' => 'text/css; charset=UTF-8', + 'js' => 'application/javascript; charset=UTF-8', + 'svg' => 'image/svg+xml', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'webp' => 'image/webp', + ]; + + header('Content-Type: ' . ($mimeTypes[$extension] ?? 'application/octet-stream')); + header('Content-Length: ' . (string) filesize($file)); + readfile($file); + + return true; +} + function resolveRepositories(array $config): array { $databaseFile = $config['database']['credentials_file']; @@ -155,17 +207,17 @@ function handleAdminRequest(string $path, string $method, BookingService $bookin flash('error', $exception->getMessage()); } - redirect('/admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? ''))); + redirect('admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? ''))); } if ($method === 'POST' && $path === '/admin/order/invoice') { try { $invoiceId = $bookingService->createInvoiceForBooking((string) ($_POST['booking_id'] ?? ''), $_POST); flash('success', 'Die Rechnung wurde erstellt.'); - redirect('/admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? '')) . '&invoice=' . urlencode($invoiceId)); + redirect('admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? '')) . '&invoice=' . urlencode($invoiceId)); } catch (Throwable $exception) { flash('error', $exception->getMessage()); - redirect('/admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? ''))); + redirect('admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? ''))); } } diff --git a/views/admin/create.php b/views/admin/create.php index f8d66c6..7f092c2 100644 --- a/views/admin/create.php +++ b/views/admin/create.php @@ -4,7 +4,7 @@
Manuelle Buchung