CluPilotCloud/resources/views/components/layouts/site.blade.php

169 lines
8.7 KiB
PHP

@props([
'title' => null,
'description' => null,
'robots' => null,
])
<!DOCTYPE html>
<html lang="de" class="scroll-smooth">
<head>
@if ($description)<meta name="description" content="{{ $description }}">@endif
@if ($robots)<meta name="robots" content="{{ $robots }}">@endif
<x-shell.head :title="$title" />
{{-- Everything below the fold arrives on scroll, which an observer decides.
Without JavaScript nothing is ever observed, so the whole page would be
a headline over a blank sheet. --}}
<noscript><style>.rv,.logline{opacity:1!important;transform:none!important}.rv .fill{transform:scaleX(1)!important}</style></noscript>
</head>
{{--
The public website.
It used to be a self-contained page with its own design system inside a
<style> block: warm paper, a serif headline, 3px corners, its own scale of
greys. It predated the token file, and by the time the portal and the
console had been rebuilt it was the only surface left speaking the old
language a visitor moved from the website to the sign-in form and arrived
somewhere that looked like a different company.
So it draws from resources/css/portal-tokens.css like everything else. Same
colours, same radii, same buttons, same small-caps label. What still differs
is density and rhythm, which is what a marketing page is allowed to differ
in: wider measure, larger type, more air between sections.
--}}
{{-- Column, so the footer sits at the bottom of the viewport on a short page
rather than halfway up it with the ground colour showing underneath. --}}
<body class="flex min-h-screen flex-col bg-bg text-body antialiased">
<div id="site-progress" class="fixed inset-x-0 top-0 z-[120] h-0.5 w-0 bg-accent" aria-hidden="true"></div>
<header
x-data="{ open: false, stuck: false }"
@scroll.window="stuck = window.scrollY > 8"
{{-- Opaque in both states. A translucent bar over a page of cards and
tables lets whatever is underneath show through the wordmark, and
backdrop-blur does not hide dark text on a light ground it only
makes it look like a rendering fault. --}}
class="fixed inset-x-0 top-0 z-[110] border-b transition-colors duration-200"
:class="stuck ? 'border-line bg-surface shadow-xs' : 'border-transparent bg-bg'"
>
<div class="mx-auto flex h-16 max-w-[1180px] items-center gap-8 px-5 sm:px-8">
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
<x-ui.logo class="size-8" />
<span class="text-md font-bold tracking-[-0.02em] text-ink">CluPilot</span>
</a>
<nav class="ml-auto hidden items-center gap-7 text-sm text-muted lg:flex">
@foreach ([
'#produkt' => 'Produkt',
'#ablauf' => 'Ablauf',
'#sicherheit' => 'Sicherheit',
'#pakete' => 'Pakete',
'#faq' => 'Fragen',
] as $href => $label)
<a href="{{ route('home') }}{{ $href }}" class="relative py-1 transition-colors hover:text-ink after:absolute after:inset-x-0 after:-bottom-px after:h-px after:origin-left after:scale-x-0 after:bg-accent after:transition-transform after:duration-300 hover:after:scale-x-100">{{ $label }}</a>
@endforeach
</nav>
<div class="ml-auto flex items-center gap-3 lg:ml-0">
<a href="{{ route('login') }}" class="hidden text-sm text-muted transition-colors hover:text-ink sm:block">Anmelden</a>
<x-ui.button href="{{ route('home') }}#pakete" variant="ink" size="sm">Pakete ansehen</x-ui.button>
<button type="button" class="-mr-1 grid size-9 place-items-center rounded text-muted transition-colors hover:bg-surface-hover hover:text-ink lg:hidden" @click="open = !open" :aria-expanded="open" aria-label="Menü">
<x-ui.icon name="menu" class="size-5" x-show="!open" />
<x-ui.icon name="x" class="size-5" x-show="open" x-cloak />
</button>
</div>
</div>
<nav x-show="open" x-cloak @click="open = false" class="border-t border-line bg-surface px-5 py-2 sm:px-8 lg:hidden">
@foreach ([
'#produkt' => 'Produkt',
'#ablauf' => 'Ablauf',
'#sicherheit' => 'Sicherheit',
'#pakete' => 'Pakete',
'#faq' => 'Fragen',
route('login') => 'Anmelden',
] as $href => $label)
<a href="{{ Str::startsWith($href, '#') ? route('home').$href : $href }}" class="flex min-h-11 items-center border-b border-line text-sm text-body last:border-0">{{ $label }}</a>
@endforeach
</nav>
</header>
<main class="flex-1">
{{ $slot }}
</main>
<footer class="border-t border-line bg-surface">
<div class="mx-auto flex max-w-[1180px] flex-col gap-5 px-5 py-9 text-sm text-muted sm:px-8 lg:flex-row lg:items-center">
<div class="flex items-center gap-2.5">
<x-ui.logo class="size-6" />
<span class="font-semibold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
</div>
<span class="lbl">© {{ date('Y') }} · Österreich</span>
<div class="flex flex-wrap gap-x-6 gap-y-2 lg:ml-auto">
<a href="{{ route('legal.impressum') }}" class="transition-colors hover:text-ink">Impressum</a>
<a href="{{ route('legal.datenschutz') }}" class="transition-colors hover:text-ink">Datenschutz</a>
<a href="{{ route('legal.agb') }}" class="transition-colors hover:text-ink">AGB</a>
<a href="{{ route('status') }}" class="transition-colors hover:text-ink">Status</a>
<a href="{{ route('login') }}" class="transition-colors hover:text-ink">Anmelden</a>
</div>
</div>
</footer>
@verbatim
<script>
// Three small behaviours, none of which Alpine expresses more clearly
// than an IntersectionObserver does: a scroll indicator, sections that
// arrive as they are reached, and figures that count up once.
(function () {
const reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
const progress = document.getElementById('site-progress');
addEventListener('scroll', () => {
const el = document.documentElement;
const max = el.scrollHeight - el.clientHeight;
progress.style.width = (max > 0 ? (scrollY / max) * 100 : 0) + '%';
}, { passive: true });
const reveal = new IntersectionObserver(entries => entries.forEach(entry => {
if (!entry.isIntersecting) return;
entry.target.classList.add('in');
reveal.unobserve(entry.target);
}), { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.rv').forEach(el => reveal.observe(el));
// Tabular figures are set on the element, so counting never shifts
// the layout around it.
const count = new IntersectionObserver(entries => entries.forEach(entry => {
if (!entry.isIntersecting) return;
count.unobserve(entry.target);
const el = entry.target;
const target = parseFloat(el.dataset.count);
const decimals = parseInt(el.dataset.decimals || 0, 10);
const format = v => v.toFixed(decimals).replace('.', ',');
if (reduce) { el.textContent = format(target); return; }
const start = performance.now();
(function tick(now) {
const p = Math.min((now - start) / 1200, 1);
el.textContent = format(target * (1 - Math.pow(1 - p, 3)));
if (p < 1) requestAnimationFrame(tick);
})(start);
}), { threshold: 0.6 });
document.querySelectorAll('[data-count]').forEach(el => count.observe(el));
// The provisioning log writes itself out, line by line.
const log = document.getElementById('deploy-log');
if (log) {
const lines = new IntersectionObserver(entries => entries.forEach(entry => {
if (!entry.isIntersecting) return;
lines.unobserve(log);
const rows = log.querySelectorAll('.logline');
rows.forEach((row, i) => setTimeout(() => row.classList.add('in'), reduce ? 0 : 240 + i * 400));
}), { threshold: 0.35 });
lines.observe(log);
}
})();
</script>
@endverbatim
</body>
</html>