Anpassung für Aufruf für proxy

This commit is contained in:
2026-05-05 11:05:03 +02:00
parent a675873437
commit bec1c8725f
11 changed files with 628 additions and 198 deletions
+6
View File
@@ -17,6 +17,12 @@ php -S 127.0.0.1:8000
Danach ist die Seite unter `http://127.0.0.1:8000` erreichbar. 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 ## Admin-Zugang
- Benutzername: `admin` - Benutzername: `admin`
+303 -94
View File
@@ -1,19 +1,20 @@
:root { :root {
--bg: #f4efe7; --bg: #f4f0e8;
--bg-panel: rgba(255, 255, 255, 0.72); --bg-panel: rgba(255, 252, 247, 0.84);
--card: #fdf9f2; --card: #fffdf8;
--surface: #ffffff; --surface: #ffffff;
--line: rgba(55, 35, 18, 0.12); --line: rgba(30, 24, 19, 0.1);
--text: #1f160f; --text: #171412;
--muted: #6d5a4d; --muted: #5a5048;
--accent: #bd5f2d; --accent: #b56a38;
--accent-deep: #7a3412; --accent-deep: #6e3413;
--accent-soft: #f1d1ba; --accent-soft: #edd8c9;
--trust: #2f5a4d;
--success: #1f6c46; --success: #1f6c46;
--error: #8a2630; --error: #8a2630;
--shadow: 0 24px 60px rgba(57, 35, 18, 0.14); --shadow: 0 24px 60px rgba(33, 24, 17, 0.1);
--radius-lg: 28px; --radius-lg: 32px;
--radius-md: 18px; --radius-md: 20px;
--radius-sm: 12px; --radius-sm: 12px;
} }
@@ -28,12 +29,12 @@ html {
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
font-family: "Avenir Next", "Segoe UI", sans-serif; font-family: "Trebuchet MS", "Gill Sans", sans-serif;
color: var(--text); color: var(--text);
background: background:
radial-gradient(circle at top left, rgba(203, 135, 77, 0.25), transparent 28%), radial-gradient(circle at top left, rgba(214, 171, 135, 0.2), transparent 28%),
radial-gradient(circle at 85% 15%, rgba(122, 52, 18, 0.12), transparent 22%), radial-gradient(circle at 90% 12%, rgba(59, 95, 82, 0.08), transparent 26%),
linear-gradient(180deg, #faf4eb 0%, var(--bg) 100%); linear-gradient(180deg, #fbf8f2 0%, var(--bg) 100%);
} }
a { a {
@@ -64,8 +65,8 @@ textarea {
top: 18px; top: 18px;
z-index: 20; z-index: 20;
margin-bottom: 32px; margin-bottom: 32px;
background: rgba(252, 248, 241, 0.82); background: rgba(252, 248, 241, 0.88);
border: 1px solid rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.75);
border-radius: 999px; border-radius: 999px;
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
box-shadow: 0 12px 30px rgba(58, 39, 24, 0.08); box-shadow: 0 12px 30px rgba(58, 39, 24, 0.08);
@@ -75,14 +76,14 @@ textarea {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
font-family: Georgia, "Times New Roman", serif; font-family: "Baskerville", "Iowan Old Style", Georgia, serif;
} }
.brand strong, .brand strong,
h1, h1,
h2, h2,
h3 { h3 {
font-family: Georgia, "Times New Roman", serif; font-family: "Baskerville", "Iowan Old Style", Georgia, serif;
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
@@ -90,7 +91,7 @@ h3 {
display: block; display: block;
color: var(--muted); color: var(--muted);
font-size: 0.8rem; font-size: 0.8rem;
font-family: "Avenir Next", "Segoe UI", sans-serif; font-family: "Trebuchet MS", "Gill Sans", sans-serif;
} }
.brand-mark { .brand-mark {
@@ -140,7 +141,7 @@ h3 {
} }
.button-secondary { .button-secondary {
background: rgba(255, 255, 255, 0.72); background: rgba(255, 255, 255, 0.78);
color: var(--text); color: var(--text);
border: 1px solid var(--line); border: 1px solid var(--line);
} }
@@ -167,8 +168,10 @@ main {
} }
.hero-section, .hero-section,
.trust-bar,
.feature-strip, .feature-strip,
.content-grid, .content-grid,
.occasion-section,
.pricing-panel, .pricing-panel,
.availability-section, .availability-section,
.booking-section, .booking-section,
@@ -183,9 +186,9 @@ main {
.hero-section { .hero-section {
display: grid; display: grid;
grid-template-columns: 1.2fr 0.9fr; grid-template-columns: 1.08fr 0.92fr;
gap: 28px; gap: 34px;
padding: 42px; padding: 48px;
} }
.eyebrow { .eyebrow {
@@ -201,8 +204,8 @@ main {
.admin-login-card h1, .admin-login-card h1,
.section-header h1 { .section-header h1 {
margin: 0; margin: 0;
font-size: clamp(2.6rem, 5vw, 4.8rem); font-size: clamp(2.8rem, 5vw, 5rem);
line-height: 0.96; line-height: 0.94;
} }
.hero-text, .hero-text,
@@ -221,25 +224,59 @@ main {
margin: 28px 0 26px; margin: 28px 0 26px;
} }
.hero-points, .hero-highlight-grid {
.check-list,
.compact-list {
margin: 0;
padding-left: 20px;
color: var(--muted);
display: 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 { .hero-card {
display: grid; display: grid;
gap: 16px; gap: 16px;
padding: 18px; padding: 22px;
background: 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); var(--card);
border-radius: 24px; border-radius: 24px;
min-height: 520px; min-height: 520px;
border: 1px solid rgba(104, 65, 39, 0.12);
} }
.hero-card-panel { .hero-card-panel {
@@ -248,7 +285,7 @@ main {
gap: 16px; gap: 16px;
padding: 16px 18px; padding: 16px 18px;
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.55); background: rgba(255, 255, 255, 0.58);
color: var(--muted); color: var(--muted);
} }
@@ -262,56 +299,142 @@ main {
place-items: center; place-items: center;
overflow: hidden; overflow: hidden;
border-radius: 24px; border-radius: 24px;
min-height: 320px; min-height: 340px;
background: linear-gradient(180deg, rgba(118, 59, 31, 0.15), rgba(33, 18, 11, 0.24)); 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; position: absolute;
width: 240px; bottom: 32px;
height: 240px; width: 220px;
border-radius: 50%; height: 24px;
background: radial-gradient(circle, rgba(255, 228, 195, 0.9) 0%, rgba(241, 179, 119, 0.15) 58%, transparent 72%); border-radius: 999px;
background: rgba(52, 38, 30, 0.12);
filter: blur(6px);
} }
.camera-body { .camera-tower {
position: relative; position: relative;
width: 280px; display: grid;
height: 200px; justify-items: center;
z-index: 1;
}
.camera-head {
position: relative;
width: 210px;
height: 230px;
border-radius: 30px; 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); 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: ""; content: "";
position: absolute; position: absolute;
inset: -22px 40px auto auto; inset: 18px 18px auto auto;
width: 90px; width: 58px;
height: 70px; height: 90px;
border-radius: 24px 24px 8px 8px; 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%); background: linear-gradient(180deg, #28211e 0%, #141111 100%);
} }
.camera-lens { .camera-lens {
position: absolute; position: absolute;
inset: 34px auto auto 78px; inset: 54px auto auto 38px;
width: 124px; width: 132px;
height: 124px; height: 132px;
border-radius: 50%; border-radius: 50%;
background: 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%); 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; border: 12px solid #4a403a;
} }
.camera-screen { .camera-flash {
position: absolute; position: absolute;
inset: 48px 28px auto auto; inset: 32px 28px auto auto;
width: 56px; width: 26px;
height: 92px; height: 26px;
border-radius: 16px; border-radius: 50%;
background: linear-gradient(180deg, rgba(255, 188, 125, 0.85), rgba(255, 240, 224, 0.22)); background: radial-gradient(circle, rgba(255, 251, 245, 1) 0%, rgba(246, 197, 151, 0.78) 58%, rgba(255, 255, 255, 0.15) 100%);
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.12); }
.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 { .feature-strip {
@@ -323,19 +446,9 @@ main {
.feature-strip article, .feature-strip article,
.content-block, .content-block,
.pricing-aside,
.booking-form-card,
.table-card, .table-card,
.admin-login-card, .admin-login-card {
.booking-info-card, padding: 28px;
.availability-card {
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--line);
border-radius: var(--radius-md);
}
.feature-strip article {
padding: 24px;
} }
.feature-strip h2, .feature-strip h2,
@@ -343,7 +456,8 @@ main {
.pricing-panel h2, .pricing-panel h2,
.booking-copy h2, .booking-copy h2,
.faq-section h2, .faq-section h2,
.table-card h2 { .table-card h2,
.section-heading h2 {
margin: 0 0 12px; margin: 0 0 12px;
font-size: clamp(1.6rem, 3vw, 2.3rem); font-size: clamp(1.6rem, 3vw, 2.3rem);
} }
@@ -360,7 +474,8 @@ main {
.stack-form span, .stack-form span,
.stack-form label, .stack-form label,
.booking-form span, .booking-form span,
.form-note { .form-note,
.occasion-grid span {
color: var(--muted); color: var(--muted);
line-height: 1.6; line-height: 1.6;
} }
@@ -374,12 +489,6 @@ main {
padding: 24px; padding: 24px;
} }
.content-block,
.table-card,
.admin-login-card {
padding: 28px;
}
.step-list { .step-list {
display: grid; display: grid;
gap: 18px; gap: 18px;
@@ -391,14 +500,41 @@ main {
.step-list li { .step-list li {
display: grid; display: grid;
gap: 6px; gap: 6px;
padding: 16px 18px; padding: 18px 20px;
border-radius: var(--radius-sm); border-radius: 18px;
background: rgba(246, 232, 221, 0.72); 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, .pricing-panel,
.availability-section, .availability-section,
.faq-section { .faq-section,
.occasion-section {
padding: 28px; padding: 28px;
} }
@@ -420,6 +556,22 @@ main {
font-size: 1.45rem; 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, .availability-list,
.faq-list, .faq-list,
.stats-grid { .stats-grid {
@@ -438,6 +590,11 @@ main {
gap: 8px; gap: 8px;
} }
.availability-card small {
color: var(--trust);
font-weight: 700;
}
.availability-card-empty { .availability-card-empty {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
@@ -456,16 +613,45 @@ main {
padding: 28px; padding: 28px;
} }
.booking-form,
.stack-form {
display: grid;
gap: 18px;
}
.form-grid { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px; gap: 16px;
} }
.booking-form, .form-grid-two {
.stack-form { grid-template-columns: repeat(2, minmax(0, 1fr));
display: grid; }
gap: 18px;
.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 { label {
@@ -491,11 +677,16 @@ textarea {
.price-summary { .price-summary {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px; gap: 16px;
padding: 18px; padding: 18px;
border-radius: 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 { .flash {
@@ -627,6 +818,10 @@ th {
margin: 0; margin: 0;
} }
.theme-admin .site-header {
background: rgba(250, 248, 244, 0.92);
}
@media (max-width: 1080px) { @media (max-width: 1080px) {
.hero-section, .hero-section,
.content-grid, .content-grid,
@@ -637,6 +832,11 @@ th {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.trust-bar,
.occasion-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stats-grid { .stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
@@ -659,6 +859,9 @@ th {
.feature-strip, .feature-strip,
.faq-list, .faq-list,
.form-grid, .form-grid,
.hero-highlight-grid,
.trust-bar,
.occasion-grid,
.price-summary { .price-summary {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -674,7 +877,8 @@ th {
.availability-section, .availability-section,
.booking-section, .booking-section,
.faq-section, .faq-section,
.admin-section { .admin-section,
.occasion-section {
padding: 20px; padding: 20px;
} }
@@ -683,6 +887,11 @@ th {
.section-header h1 { .section-header h1 {
font-size: 2.35rem; font-size: 2.35rem;
} }
.price-summary-total {
border-left: 0;
padding-left: 0;
}
} }
@media (max-width: 520px) { @media (max-width: 520px) {
+3
View File
@@ -3,6 +3,9 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'app' => [
'base_path' => getenv('FOTOBOX_BASE_PATH') ?: '',
],
'company' => [ 'company' => [
'name' => 'Fotobox Moments', 'name' => 'Fotobox Moments',
'tagline' => 'Fotobox-Vermietung fuer Hochzeiten, Geburtstage und Firmenevents', 'tagline' => 'Fotobox-Vermietung fuer Hochzeiten, Geburtstage und Firmenevents',
+73 -2
View File
@@ -2,6 +2,16 @@
declare(strict_types=1); 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 function render(string $view, array $data = []): void
{ {
extract($data, EXTR_SKIP); extract($data, EXTR_SKIP);
@@ -9,14 +19,75 @@ function render(string $view, array $data = []): void
require dirname(__DIR__, 1) . '/../views/layout.php'; 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 function asset(string $path): string
{ {
return '/' . ltrim($path, '/'); return url($path);
} }
function redirect(string $path): void function redirect(string $path): void
{ {
header('Location: ' . $path); header('Location: ' . url($path));
exit; exit;
} }
+56 -4
View File
@@ -14,6 +14,7 @@ session_start();
function runApplication(): void function runApplication(): void
{ {
$config = require dirname(__DIR__) . '/config.php'; $config = require dirname(__DIR__) . '/config.php';
setAppConfig($config);
[$bookingRepository, $invoiceRepository] = resolveRepositories($config); [$bookingRepository, $invoiceRepository] = resolveRepositories($config);
@@ -21,7 +22,11 @@ function runApplication(): void
$invoicePdfService = new InvoicePdfService($config); $invoicePdfService = new InvoicePdfService($config);
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $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') { if ($method === 'POST' && $path === '/book') {
handlePublicBooking($bookingService); handlePublicBooking($bookingService);
@@ -52,6 +57,53 @@ function runApplication(): void
renderHome($bookingService, $config); 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 function resolveRepositories(array $config): array
{ {
$databaseFile = $config['database']['credentials_file']; $databaseFile = $config['database']['credentials_file'];
@@ -155,17 +207,17 @@ function handleAdminRequest(string $path, string $method, BookingService $bookin
flash('error', $exception->getMessage()); 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') { if ($method === 'POST' && $path === '/admin/order/invoice') {
try { try {
$invoiceId = $bookingService->createInvoiceForBooking((string) ($_POST['booking_id'] ?? ''), $_POST); $invoiceId = $bookingService->createInvoiceForBooking((string) ($_POST['booking_id'] ?? ''), $_POST);
flash('success', 'Die Rechnung wurde erstellt.'); 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) { } catch (Throwable $exception) {
flash('error', $exception->getMessage()); flash('error', $exception->getMessage());
redirect('/admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? ''))); redirect('admin/order?id=' . urlencode((string) ($_POST['booking_id'] ?? '')));
} }
} }
+2 -2
View File
@@ -4,7 +4,7 @@
<p class="eyebrow">Manuelle Buchung</p> <p class="eyebrow">Manuelle Buchung</p>
<h1>Bestellung fuer Kunden anlegen</h1> <h1>Bestellung fuer Kunden anlegen</h1>
</div> </div>
<a class="button-secondary" href="/admin">Zurueck zum Dashboard</a> <a class="button-secondary" href="<?= h(url('admin')) ?>">Zurueck zum Dashboard</a>
</div> </div>
<?php if (!empty($flashSuccess)): ?> <?php if (!empty($flashSuccess)): ?>
@@ -14,7 +14,7 @@
<div class="flash flash-error"><?= h((string) $flashError) ?></div> <div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?> <?php endif; ?>
<form method="post" action="/admin/create" class="booking-form admin-form" data-day-rate="<?= h((string) $defaults['price_per_day_cents']) ?>"> <form method="post" action="<?= h(url('admin/create')) ?>" class="booking-form admin-form" data-day-rate="<?= h((string) $defaults['price_per_day_cents']) ?>">
<div class="form-grid"> <div class="form-grid">
<label> <label>
<span>Name</span> <span>Name</span>
+3 -3
View File
@@ -4,7 +4,7 @@
<p class="eyebrow">Dashboard</p> <p class="eyebrow">Dashboard</p>
<h1>Anfragen, Buchungen und Rechnungen</h1> <h1>Anfragen, Buchungen und Rechnungen</h1>
</div> </div>
<a class="button-primary" href="/admin/create">Bestellung fuer Kunden anlegen</a> <a class="button-primary" href="<?= h(url('admin/create')) ?>">Bestellung fuer Kunden anlegen</a>
</div> </div>
<?php if (!empty($flashSuccess)): ?> <?php if (!empty($flashSuccess)): ?>
@@ -62,7 +62,7 @@
<tbody> <tbody>
<?php foreach ($bookings as $booking): ?> <?php foreach ($bookings as $booking): ?>
<tr> <tr>
<td><a href="/admin/order?id=<?= h(urlencode($booking['id'])) ?>"><?= h($booking['reference']) ?></a></td> <td><a href="<?= h(url('admin/order?id=' . urlencode($booking['id']))) ?>"><?= h($booking['reference']) ?></a></td>
<td><?= h($booking['customer']['name']) ?></td> <td><?= h($booking['customer']['name']) ?></td>
<td><?= h(formatDate($booking['start_date'])) ?> - <?= h(formatDate($booking['end_date'])) ?></td> <td><?= h(formatDate($booking['start_date'])) ?> - <?= h(formatDate($booking['end_date'])) ?></td>
<td><?= h($booking['status_label']) ?></td> <td><?= h($booking['status_label']) ?></td>
@@ -99,7 +99,7 @@
<tbody> <tbody>
<?php foreach ($invoices as $invoice): ?> <?php foreach ($invoices as $invoice): ?>
<tr> <tr>
<td><a href="/admin/invoice/pdf?id=<?= h(urlencode($invoice['id'])) ?>" target="_blank"><?= h($invoice['invoice_number']) ?></a></td> <td><a href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank"><?= h($invoice['invoice_number']) ?></a></td>
<td><?= h($invoice['booking_id']) ?></td> <td><?= h($invoice['booking_id']) ?></td>
<td><?= h(formatDate($invoice['due_date'])) ?></td> <td><?= h(formatDate($invoice['due_date'])) ?></td>
<td><?= h($invoice['payment_method_label']) ?></td> <td><?= h($invoice['payment_method_label']) ?></td>
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="flash flash-error"><?= h((string) $flashError) ?></div> <div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?> <?php endif; ?>
<form method="post" action="/admin/login" class="stack-form"> <form method="post" action="<?= h(url('admin/login')) ?>" class="stack-form">
<label> <label>
<span>Benutzername</span> <span>Benutzername</span>
<input type="text" name="username" value="admin" required> <input type="text" name="username" value="admin" required>
+4 -4
View File
@@ -4,7 +4,7 @@
<p class="eyebrow">Auftragsdetail</p> <p class="eyebrow">Auftragsdetail</p>
<h1><?= h($booking['reference']) ?></h1> <h1><?= h($booking['reference']) ?></h1>
</div> </div>
<a class="button-secondary" href="/admin">Zurueck zum Dashboard</a> <a class="button-secondary" href="<?= h(url('admin')) ?>">Zurueck zum Dashboard</a>
</div> </div>
<?php if (!empty($flashSuccess)): ?> <?php if (!empty($flashSuccess)): ?>
@@ -34,7 +34,7 @@
<article class="table-card"> <article class="table-card">
<h2>Verwaltung</h2> <h2>Verwaltung</h2>
<form method="post" action="/admin/order/update" class="stack-form"> <form method="post" action="<?= h(url('admin/order/update')) ?>" class="stack-form">
<input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>"> <input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>">
<label> <label>
<span>Status</span> <span>Status</span>
@@ -75,10 +75,10 @@
<li>Gesamtbetrag <?= h(formatCurrency((int) $invoice['total_cents'])) ?></li> <li>Gesamtbetrag <?= h(formatCurrency((int) $invoice['total_cents'])) ?></li>
<li>Zahlungsart <?= h($invoice['payment_method_label']) ?></li> <li>Zahlungsart <?= h($invoice['payment_method_label']) ?></li>
</ul> </ul>
<a class="button-secondary" href="/admin/invoice/pdf?id=<?= h(urlencode($invoice['id'])) ?>" target="_blank">PDF oeffnen</a> <a class="button-secondary" href="<?= h(url('admin/invoice/pdf?id=' . urlencode($invoice['id']))) ?>" target="_blank">PDF oeffnen</a>
<?php else: ?> <?php else: ?>
<p>Fuer diesen Auftrag wurde noch keine Rechnung erstellt.</p> <p>Fuer diesen Auftrag wurde noch keine Rechnung erstellt.</p>
<form method="post" action="/admin/order/invoice" class="stack-form"> <form method="post" action="<?= h(url('admin/order/invoice')) ?>" class="stack-form">
<input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>"> <input type="hidden" name="booking_id" value="<?= h($booking['id']) ?>">
<label> <label>
<span>Faelligkeitsdatum</span> <span>Faelligkeitsdatum</span>
+132 -45
View File
@@ -5,32 +5,58 @@ $company = $config['company'];
<section class="hero-section"> <section class="hero-section">
<div class="hero-copy"> <div class="hero-copy">
<p class="eyebrow">Fotobox-Vermietung neu gedacht</p> <p class="eyebrow">Fotobox-Vermietung neu gedacht</p>
<h1>Die Fotobox fuer Hochzeiten, Geburtstage und Firmenevents</h1> <h1>Fotobox mieten. Online anfragen. Fotos direkt aufs Handy.</h1>
<p class="hero-text"> <p class="hero-text">
Unkompliziert mieten, schnell aufbauen, kinderleicht bedienen und alle Bilder direkt digital sichern. Fuer Hochzeiten, Geburtstage, Firmenfeiern und Jubilaeen: hochwertige Fotobox-Technik,
Unsere Fotobox verbindet hochwertige Technik mit einem klaren Buchungsprozess ohne Shop-Chaos. einfache Bedienung, flexible Lieferung und ein klarer Buchungsprozess ohne Shop-Chaos.
</p> </p>
<div class="hero-actions"> <div class="hero-actions">
<a class="button-primary" href="#buchung">Verfuegbarkeit pruefen</a> <a class="button-primary" href="#buchung">Verfuegbarkeit pruefen</a>
<a class="button-secondary" href="#ablauf">So funktioniert die Miete</a> <a class="button-secondary" href="#ablauf">So funktioniert die Miete</a>
</div> </div>
<ul class="hero-points"> <div class="hero-highlight-grid">
<li>Spiegelreflexkamera, Studioblitz und Softbox</li> <article class="hero-highlight-card">
<li>WLAN-Download direkt aufs Handy</li> <span>Technik</span>
<li>Selbstabholung oder Lieferung mit Aufbau</li> <strong>DSLR, Blitz und Softbox</strong>
<li>99,99 EUR pro Kalendertag</li> </article>
</ul> <article class="hero-highlight-card">
<span>Sharing</span>
<strong>WLAN-Download direkt vor Ort</strong>
</article>
<article class="hero-highlight-card">
<span>Logistik</span>
<strong>Abholung oder Lieferung</strong>
</article>
<article class="hero-highlight-card">
<span>Preis</span>
<strong>99,99 EUR pro Kalendertag</strong>
</article>
</div>
</div> </div>
<div class="hero-card"> <div class="hero-card">
<div class="hero-card-panel hero-card-panel-top"> <div class="hero-card-panel hero-card-panel-top">
<span>Eventbereit in wenigen Minuten</span> <span>Produktionsreif fuer dein Event</span>
<strong>Abholung ab 17:00 Uhr</strong> <strong>einsatzklar in wenigen Minuten</strong>
</div> </div>
<div class="hero-card-visual"> <div class="hero-card-visual">
<div class="camera-glow"></div> <div class="device-plinth"></div>
<div class="camera-body"> <div class="photo-strip photo-strip-left">
<span>WLAN</span>
<span>Live</span>
<span>Shots</span>
</div>
<div class="camera-tower">
<div class="camera-head">
<div class="camera-lens"></div> <div class="camera-lens"></div>
<div class="camera-screen"></div> <div class="camera-flash"></div>
</div>
<div class="camera-stand"></div>
<div class="camera-base"></div>
</div>
<div class="photo-strip photo-strip-right">
<span>Reel</span>
<span>Share</span>
<span>Smile</span>
</div> </div>
</div> </div>
<div class="hero-card-panel"> <div class="hero-card-panel">
@@ -40,6 +66,25 @@ $company = $config['company'];
</div> </div>
</section> </section>
<section class="trust-bar">
<article>
<span>Tagessatz</span>
<strong>99,99 EUR / Tag</strong>
</article>
<article>
<span>Bilduebergabe</span>
<strong>alle Fotos digital</strong>
</article>
<article>
<span>Zahlung</span>
<strong>Rechnung oder PayPal</strong>
</article>
<article>
<span>Verfuegbarkeit</span>
<strong>online und verwaltbar</strong>
</article>
</section>
<section class="feature-strip" id="leistungen"> <section class="feature-strip" id="leistungen">
<article> <article>
<h2>Technik, die nicht zickt</h2> <h2>Technik, die nicht zickt</h2>
@@ -61,19 +106,19 @@ $company = $config['company'];
<h2>So laeuft die Miete ab</h2> <h2>So laeuft die Miete ab</h2>
<ol class="step-list"> <ol class="step-list">
<li> <li>
<strong>1. Zeitraum waehlen</strong> <strong><span class="step-number">1</span> Zeitraum waehlen</strong>
<span>Du waehlst Mietbeginn und Mietende und siehst sofort die voraussichtlichen Kosten.</span> <span>Du waehlst Mietbeginn und Mietende und siehst sofort die voraussichtlichen Kosten.</span>
</li> </li>
<li> <li>
<strong>2. Leistung festlegen</strong> <strong><span class="step-number">2</span> Leistung festlegen</strong>
<span>Selbstabholung, Lieferung mit Aufbau oder Vor-Ort-Betreuung passend zu deinem Event.</span> <span>Selbstabholung, Lieferung mit Aufbau oder Vor-Ort-Betreuung passend zu deinem Event.</span>
</li> </li>
<li> <li>
<strong>3. Anfrage absenden</strong> <strong><span class="step-number">3</span> Anfrage absenden</strong>
<span>Wir speichern alle Kundendaten und bereiten auf Wunsch direkt die Rechnungsabwicklung vor.</span> <span>Wir speichern alle Kundendaten und bereiten auf Wunsch direkt die Rechnungsabwicklung vor.</span>
</li> </li>
<li> <li>
<strong>4. Fotos geniessen</strong> <strong><span class="step-number">4</span> Fotos geniessen</strong>
<span>Am Eventtag steht die Box bereit und danach bekommst du alle Bilder digital zur Weitergabe.</span> <span>Am Eventtag steht die Box bereit und danach bekommst du alle Bilder digital zur Weitergabe.</span>
</li> </li>
</ol> </ol>
@@ -117,6 +162,19 @@ $company = $config['company'];
</div> </div>
</section> </section>
<section class="occasion-section">
<div class="section-heading">
<p class="eyebrow">Anlaesse</p>
<h2>Passend fuer kleine Feiern und grosse Events</h2>
</div>
<div class="occasion-grid">
<article><strong>Hochzeiten</strong><span>emotionale Erinnerungen, direkt teilbar</span></article>
<article><strong>Geburtstage</strong><span>einfacher Aufbau, unkomplizierter Spass</span></article>
<article><strong>Firmenfeiern</strong><span>sauberer Ablauf mit Rechnung und Verwaltung</span></article>
<article><strong>Jubilaeen</strong><span>hochwertige Bilder ohne Fotostress</span></article>
</div>
</section>
<section class="availability-section"> <section class="availability-section">
<div> <div>
<p class="eyebrow">Online-Verfuegbarkeit</p> <p class="eyebrow">Online-Verfuegbarkeit</p>
@@ -162,7 +220,53 @@ $company = $config['company'];
<div class="flash flash-error"><?= h((string) $flashError) ?></div> <div class="flash flash-error"><?= h((string) $flashError) ?></div>
<?php endif; ?> <?php endif; ?>
<form method="post" action="/book" class="booking-form" data-day-rate="<?= h((string) $dayRate) ?>"> <form method="post" action="<?= h(url('book')) ?>" class="booking-form" data-day-rate="<?= h((string) $dayRate) ?>">
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Schritt 1</span>
<h3>Zeitraum waehlen</h3>
</div>
<div class="form-grid form-grid-two">
<label>
<span>Mietbeginn</span>
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($old['start_date'] ?? '')) ?>" required>
</label>
<label>
<span>Mietende</span>
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
</label>
</div>
</div>
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Schritt 2</span>
<h3>Leistung und Zahlung</h3>
</div>
<div class="form-grid form-grid-two">
<label>
<span>Lieferart</span>
<select name="delivery_mode">
<option value="self_pickup" <?= selected((string) ($old['delivery_mode'] ?? 'self_pickup'), 'self_pickup') ?>>Selbstabholung</option>
<option value="delivery_setup" <?= selected((string) ($old['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option>
<option value="on_site_support" <?= selected((string) ($old['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</option>
</select>
</label>
<label>
<span>Zahlungsart</span>
<select name="payment_method">
<option value="invoice_transfer" <?= selected((string) ($old['payment_method'] ?? 'invoice_transfer'), 'invoice_transfer') ?>>Rechnung / Ueberweisung</option>
<option value="paypal" <?= selected((string) ($old['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
</select>
</label>
</div>
</div>
<div class="form-section">
<div class="form-section-header">
<span class="form-step">Schritt 3</span>
<h3>Kontaktdaten</h3>
</div>
<div class="form-grid"> <div class="form-grid">
<label> <label>
<span>Name</span> <span>Name</span>
@@ -196,34 +300,13 @@ $company = $config['company'];
<span>Anlass</span> <span>Anlass</span>
<input type="text" name="event_type" value="<?= h((string) ($old['event_type'] ?? '')) ?>" placeholder="z. B. Hochzeit oder Sommerfest"> <input type="text" name="event_type" value="<?= h((string) ($old['event_type'] ?? '')) ?>" placeholder="z. B. Hochzeit oder Sommerfest">
</label> </label>
<label> <label class="field-full">
<span>Veranstaltungsort</span> <span>Veranstaltungsort</span>
<input type="text" name="event_location" value="<?= h((string) ($old['event_location'] ?? '')) ?>" placeholder="Location oder Stadt"> <input type="text" name="event_location" value="<?= h((string) ($old['event_location'] ?? '')) ?>" placeholder="Location oder Stadt">
</label> </label>
<label>
<span>Mietbeginn</span>
<input type="date" name="start_date" data-booking-start value="<?= h((string) ($old['start_date'] ?? '')) ?>" required>
</label>
<label>
<span>Mietende</span>
<input type="date" name="end_date" data-booking-end value="<?= h((string) ($old['end_date'] ?? '')) ?>" required>
</label>
<label>
<span>Lieferart</span>
<select name="delivery_mode">
<option value="self_pickup" <?= selected((string) ($old['delivery_mode'] ?? 'self_pickup'), 'self_pickup') ?>>Selbstabholung</option>
<option value="delivery_setup" <?= selected((string) ($old['delivery_mode'] ?? ''), 'delivery_setup') ?>>Lieferung und Aufbau</option>
<option value="on_site_support" <?= selected((string) ($old['delivery_mode'] ?? ''), 'on_site_support') ?>>Lieferung, Aufbau und Vor-Ort-Betreuung</option>
</select>
</label>
<label>
<span>Zahlungsart</span>
<select name="payment_method">
<option value="invoice_transfer" <?= selected((string) ($old['payment_method'] ?? 'invoice_transfer'), 'invoice_transfer') ?>>Rechnung / Ueberweisung</option>
<option value="paypal" <?= selected((string) ($old['payment_method'] ?? ''), 'paypal') ?>>PayPal</option>
</select>
</label>
</div> </div>
</div>
<label> <label>
<span>Nachricht</span> <span>Nachricht</span>
<textarea name="notes_customer" rows="4" placeholder="Sonderwuensche, Lieferdetails oder Aufbauhinweise"><?= h((string) ($old['notes_customer'] ?? '')) ?></textarea> <textarea name="notes_customer" rows="4" placeholder="Sonderwuensche, Lieferdetails oder Aufbauhinweise"><?= h((string) ($old['notes_customer'] ?? '')) ?></textarea>
@@ -235,13 +318,17 @@ $company = $config['company'];
<strong data-summary-days>Noch nicht gewaehlt</strong> <strong data-summary-days>Noch nicht gewaehlt</strong>
</div> </div>
<div> <div>
<span>Tagespreis</span>
<strong><?= h(formatCurrency($dayRate)) ?></strong>
</div>
<div class="price-summary-total">
<span>Gesamtpreis</span> <span>Gesamtpreis</span>
<strong data-summary-total><?= h(formatCurrency($dayRate)) ?></strong> <strong data-summary-total><?= h(formatCurrency($dayRate)) ?></strong>
</div> </div>
</div> </div>
<button type="submit" class="button-primary button-block">Anfrage absenden</button> <button type="submit" class="button-primary button-block">Buchungsanfrage senden</button>
<p class="form-note">Mit dem Absenden wird eine verwaltbare Anfrage angelegt. Die finale Bestaetigung erfolgt durch den Verwalter.</p> <p class="form-note">Keine Sofortabbuchung. Der Verwalter bestaetigt den Termin, pflegt die Buchung und kann direkt eine Rechnung erzeugen.</p>
</form> </form>
</div> </div>
</section> </section>
+8 -6
View File
@@ -1,6 +1,8 @@
<?php <?php
$metaTitle = isset($pageTitle) ? $pageTitle . ' | Fotobox Moments' : 'Fotobox Moments'; $metaTitle = isset($pageTitle) ? $pageTitle . ' | Fotobox Moments' : 'Fotobox Moments';
$isAdminArea = str_contains($viewPath, '/admin/'); $isAdminArea = str_contains($viewPath, '/admin/');
$styleVersion = is_file(dirname(__DIR__) . '/assets/styles.css') ? (string) filemtime(dirname(__DIR__) . '/assets/styles.css') : '1';
$scriptVersion = is_file(dirname(__DIR__) . '/assets/app.js') ? (string) filemtime(dirname(__DIR__) . '/assets/app.js') : '1';
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
@@ -9,13 +11,13 @@ $isAdminArea = str_contains($viewPath, '/admin/');
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= h($metaTitle) ?></title> <title><?= h($metaTitle) ?></title>
<meta name="description" content="Fotobox mieten mit einfacher Online-Anfrage, flexibler Lieferung und kompletter Admin-Verwaltung."> <meta name="description" content="Fotobox mieten mit einfacher Online-Anfrage, flexibler Lieferung und kompletter Admin-Verwaltung.">
<link rel="stylesheet" href="<?= h(asset('assets/styles.css')) ?>"> <link rel="stylesheet" href="<?= h(asset('assets/styles.css')) ?>?v=<?= h($styleVersion) ?>">
<script defer src="<?= h(asset('assets/app.js')) ?>"></script> <script defer src="<?= h(asset('assets/app.js')) ?>?v=<?= h($scriptVersion) ?>"></script>
</head> </head>
<body class="<?= $isAdminArea ? 'theme-admin' : 'theme-public' ?>"> <body class="<?= $isAdminArea ? 'theme-admin' : 'theme-public' ?>">
<div class="page-shell"> <div class="page-shell">
<header class="site-header"> <header class="site-header">
<a class="brand" href="<?= $isAdminArea ? '/admin' : '/' ?>"> <a class="brand" href="<?= h($isAdminArea ? url('admin') : url('/')) ?>">
<span class="brand-mark">FM</span> <span class="brand-mark">FM</span>
<span> <span>
<strong>Fotobox Moments</strong> <strong>Fotobox Moments</strong>
@@ -24,9 +26,9 @@ $isAdminArea = str_contains($viewPath, '/admin/');
</a> </a>
<nav class="site-nav"> <nav class="site-nav">
<?php if ($isAdminArea): ?> <?php if ($isAdminArea): ?>
<a href="/admin">Dashboard</a> <a href="<?= h(url('admin')) ?>">Dashboard</a>
<a href="/admin/create">Bestellung anlegen</a> <a href="<?= h(url('admin/create')) ?>">Bestellung anlegen</a>
<form method="post" action="/admin/logout"> <form method="post" action="<?= h(url('admin/logout')) ?>">
<button type="submit" class="ghost-button">Logout</button> <button type="submit" class="ghost-button">Logout</button>
</form> </form>
<?php else: ?> <?php else: ?>