Page: Signup Glass — Volt + Pest tests + CSS additions
- pages/auth/register: full re-port of CluPilot Signup Glass.html with 3-step stepper, SSO, name+email+workspace+password form, live slug preview, password meter (4 rules), terms checkbox, right-pane trial card + 4-benefit list + customer quote + logo strip. - CSS additions: clu-stepper, clu-field-row, clu-input-suffix, clu-slug-preview, clu-pw-meter, clu-pw-rules, clu-trial-*, clu-benefit-*, clu-quote-*, clu-avatar, clu-logos. - tests/Feature/Auth/SignupPageTest.php: 6 tests, all green (renders chrome, slug preview, empty-form rejects, weak password, creates user + logs in, duplicate email).master
parent
5287b0024d
commit
78edcef3da
|
|
@ -403,4 +403,228 @@
|
|||
.clu-pill.danger { color: var(--color-danger); background: var(--color-danger-soft); border-color: rgba(219,59,59,0.28); }
|
||||
.clu-pill.accent { color: var(--color-accent); background: var(--color-accent-soft); border-color: rgba(59,111,242,0.28); }
|
||||
.clu-pill.muted { color: var(--color-muted); background: rgba(0,0,0,0.04); border-color: var(--color-hairline); }
|
||||
|
||||
/* ───── Signup-specific ───── */
|
||||
|
||||
/* Stepper */
|
||||
.clu-stepper {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-top: 24px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
.clu-step { display: flex; align-items: center; gap: 8px; }
|
||||
.clu-step-num {
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-hairline);
|
||||
background: rgba(255,255,255,0.6);
|
||||
display: grid; place-items: center;
|
||||
font-size: 11px; font-weight: 600;
|
||||
color: var(--color-muted-2);
|
||||
transition: all .15s;
|
||||
}
|
||||
.clu-step.active .clu-step-num { background: var(--color-accent); border-color: var(--color-accent); color: #fff; box-shadow: 0 4px 12px -4px rgba(59,111,242,0.6); }
|
||||
.clu-step.done .clu-step-num { background: var(--color-success); border-color: var(--color-success); color: #fff; }
|
||||
.clu-step-label {
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.clu-step.active .clu-step-label { color: var(--color-fg); }
|
||||
.clu-step-line { flex: 1; height: 1px; background: var(--color-hairline); min-width: 14px; max-width: 28px; }
|
||||
|
||||
/* Input variants */
|
||||
.clu-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.clu-hint {
|
||||
font-size: 10.5px; font-weight: 500;
|
||||
color: var(--color-muted-2);
|
||||
font-family: var(--font-mono);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.clu-input.with-suffix { padding-right: 0; }
|
||||
.clu-input-suffix {
|
||||
height: 100%;
|
||||
display: flex; align-items: center;
|
||||
padding: 0 13px;
|
||||
border-left: 1px solid var(--color-hairline);
|
||||
color: var(--color-muted-2);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11.5px;
|
||||
background: rgba(255,255,255,0.35);
|
||||
border-top-right-radius: var(--radius-xs);
|
||||
border-bottom-right-radius: var(--radius-xs);
|
||||
}
|
||||
.clu-input-check { color: var(--color-success); display: none; flex: none; }
|
||||
.clu-input.valid .clu-input-check { display: inline-flex; }
|
||||
|
||||
/* Slug preview */
|
||||
.clu-slug-preview {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.clu-slug-preview b { color: var(--color-accent); font-weight: 600; }
|
||||
|
||||
/* Password meter + rules */
|
||||
.clu-pw-meter {
|
||||
display: flex; gap: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.clu-pw-meter span {
|
||||
flex: 1; height: 3px;
|
||||
border-radius: 2px;
|
||||
background: rgba(30,35,60,0.10);
|
||||
transition: background .2s;
|
||||
}
|
||||
.clu-pw-meter.s1 span:nth-child(-n+1) { background: var(--color-danger); }
|
||||
.clu-pw-meter.s2 span:nth-child(-n+2) { background: var(--color-warning); }
|
||||
.clu-pw-meter.s3 span:nth-child(-n+3) { background: var(--color-accent); }
|
||||
.clu-pw-meter.s4 span { background: var(--color-success); }
|
||||
.clu-pw-rules {
|
||||
display: flex; flex-wrap: wrap; gap: 6px 12px;
|
||||
margin-top: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.clu-pw-rule { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.clu-pw-rule .clu-pw-bullet {
|
||||
width: 5px; height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-muted-2);
|
||||
transition: background .15s;
|
||||
}
|
||||
.clu-pw-rule.ok { color: var(--color-success); }
|
||||
.clu-pw-rule.ok .clu-pw-bullet { background: var(--color-success); }
|
||||
|
||||
/* Trial card */
|
||||
.clu-trial-card {
|
||||
background: var(--color-glass);
|
||||
border: 1px solid var(--color-glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 18px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(14px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(160%);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
|
||||
}
|
||||
.clu-trial-card::before {
|
||||
content: "";
|
||||
position: absolute; top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-2));
|
||||
}
|
||||
.clu-trial-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
|
||||
.clu-trial-tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px; font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
background: var(--color-accent-soft);
|
||||
padding: 3px 9px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(59,111,242,0.25);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.clu-trial-count {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-muted);
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.clu-trial-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.25;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.clu-trial-title em {
|
||||
font-style: normal;
|
||||
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 700;
|
||||
}
|
||||
.clu-trial-sub {
|
||||
color: var(--color-muted);
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Benefit list */
|
||||
.clu-benefit-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 14px; }
|
||||
.clu-benefit { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.clu-benefit-ico {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 9px;
|
||||
background: var(--color-accent-soft);
|
||||
border: 1px solid rgba(59,111,242,0.22);
|
||||
color: var(--color-accent);
|
||||
display: grid; place-items: center;
|
||||
flex: none;
|
||||
}
|
||||
.clu-benefit-ico.success { background: var(--color-success-soft); border-color: rgba(21,160,106,0.22); color: var(--color-success); }
|
||||
.clu-benefit-ico.purple { background: rgba(124,92,255,0.12); border-color: rgba(124,92,255,0.25); color: var(--color-accent-2); }
|
||||
.clu-benefit-ico.warn { background: var(--color-warning-soft); border-color: rgba(194,130,15,0.22); color: var(--color-warning); }
|
||||
.clu-benefit-title { font-size: 13.5px; font-weight: 600; color: var(--color-fg); }
|
||||
.clu-benefit-desc { font-size: 12.5px; color: var(--color-muted); margin-top: 2px; line-height: 1.5; }
|
||||
|
||||
/* Quote card */
|
||||
.clu-quote-card {
|
||||
background: var(--color-glass);
|
||||
border: 1px solid var(--color-glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 18px 20px;
|
||||
margin-top: auto;
|
||||
backdrop-filter: blur(14px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(160%);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
|
||||
}
|
||||
.clu-quote-text { font-size: 13.5px; color: var(--color-fg); line-height: 1.55; }
|
||||
.clu-quote-text::before { content: "„"; color: var(--color-accent); font-size: 24px; line-height: 0; margin-right: 4px; vertical-align: -6px; font-weight: 600; }
|
||||
.clu-quote-text::after { content: """; color: var(--color-accent); font-weight: 600; }
|
||||
.clu-quote-author { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
|
||||
.clu-avatar {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-success), #2a9d6f);
|
||||
display: grid; place-items: center;
|
||||
color: #fff; font-weight: 700; font-size: 12px;
|
||||
font-family: var(--font-mono);
|
||||
flex: none;
|
||||
}
|
||||
.clu-author-name { font-size: 12.5px; color: var(--color-fg); font-weight: 600; }
|
||||
.clu-author-role { font-size: 11px; color: var(--color-muted); font-family: var(--font-mono); }
|
||||
|
||||
/* Logo strip */
|
||||
.clu-logos {
|
||||
display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.clu-logo-pill {
|
||||
padding: 5px 11px;
|
||||
border: 1px solid var(--color-glass-border);
|
||||
border-radius: 7px;
|
||||
background: var(--color-glass);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* Top-link helper */
|
||||
.clu-top-link {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
color: var(--color-muted);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.clu-top-link a { color: var(--color-accent); font-weight: 600; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use App\Models\User;
|
|||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
|
@ -17,12 +18,38 @@ new #[Layout('layouts.login')] class extends Component
|
|||
public string $password = '';
|
||||
public bool $terms = false;
|
||||
|
||||
public function getSlugProperty(): string
|
||||
{
|
||||
return Str::slug($this->workspace) ?: 'dein-team';
|
||||
}
|
||||
|
||||
public function getEmailValidProperty(): bool
|
||||
{
|
||||
return (bool) preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/', $this->email);
|
||||
}
|
||||
|
||||
public function getPwChecksProperty(): array
|
||||
{
|
||||
$v = $this->password;
|
||||
return [
|
||||
'len' => mb_strlen($v) >= 10,
|
||||
'upper' => (bool) preg_match('/[A-Z]/', $v),
|
||||
'num' => (bool) preg_match('/[0-9]/', $v),
|
||||
'sym' => (bool) preg_match('/[^A-Za-z0-9]/', $v),
|
||||
];
|
||||
}
|
||||
|
||||
public function getPwScoreProperty(): int
|
||||
{
|
||||
return count(array_filter($this->pwChecks));
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->validate([
|
||||
'firstname' => ['required', 'string', 'max:255'],
|
||||
'lastname' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
|
||||
'workspace' => ['required', 'string', 'max:255'],
|
||||
'password' => ['required', 'string', Rules\Password::min(10)],
|
||||
'terms' => ['accepted'],
|
||||
|
|
@ -41,287 +68,283 @@ new #[Layout('layouts.login')] class extends Component
|
|||
}
|
||||
}; ?>
|
||||
|
||||
<div class="clu-login-grid">
|
||||
<div class="clu-auth-grid">
|
||||
|
||||
{{-- ===== LEFT: FORM ===== --}}
|
||||
<section class="clu-form-side flex flex-col min-h-screen">
|
||||
{{-- ============ LEFT: FORM ============ --}}
|
||||
<section class="clu-glass flex flex-col" style="padding:30px 44px;min-height:calc(100vh - 28px);">
|
||||
|
||||
{{-- Top row --}}
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap gap-y-2">
|
||||
<div class="flex items-center gap-[10px] shrink-0">
|
||||
<div class="clu-brand-mark w-[30px] h-[30px] rounded-[8px]" aria-hidden="true"></div>
|
||||
<div class="font-semibold text-[15px] tracking-[-0.01em]">
|
||||
CluPilot<span class="text-t2 font-medium ml-[5px] text-[11.5px]">v2.4</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clu-login-status text-[12.5px] text-t2 shrink-0">
|
||||
Schon Konto?
|
||||
<a href="{{ route('login') }}" wire:navigate class="ml-[6px] font-medium text-akzent hover:underline">Anmelden →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stepper --}}
|
||||
<nav class="clu-stepper" aria-label="Registrierungs-Schritte">
|
||||
<div class="clu-step active">
|
||||
<div class="clu-step-num" aria-current="step">1</div>
|
||||
<div class="clu-step-label">Konto</div>
|
||||
</div>
|
||||
<div class="clu-step-line" role="separator"></div>
|
||||
<div class="clu-step">
|
||||
<div class="clu-step-num">2</div>
|
||||
<div class="clu-step-label">Workspace</div>
|
||||
</div>
|
||||
<div class="clu-step-line" role="separator"></div>
|
||||
<div class="clu-step">
|
||||
<div class="clu-step-num">3</div>
|
||||
<div class="clu-step-label">Erster Server</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{-- Form wrap --}}
|
||||
<div class="my-auto flex flex-col gap-[22px] w-full py-6 sm:py-10 mx-auto" style="max-width:420px;">
|
||||
|
||||
<header>
|
||||
<h1 class="m-0 text-[24px] font-semibold tracking-[-0.02em] leading-[1.2]">14 Tage kostenlos testen</h1>
|
||||
<p class="mt-[6px] text-[13.5px] text-t2">Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.</p>
|
||||
</header>
|
||||
|
||||
{{-- SSO --}}
|
||||
<div class="clu-sso-row">
|
||||
<button class="clu-sso-btn" type="button">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/>
|
||||
</svg>
|
||||
Mit Google fortfahren
|
||||
</button>
|
||||
<button class="clu-sso-btn" type="button">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/>
|
||||
</svg>
|
||||
Mit GitHub fortfahren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clu-divider">oder mit E-Mail</div>
|
||||
|
||||
{{-- Form --}}
|
||||
<form wire:submit="register" class="flex flex-col gap-[14px]">
|
||||
|
||||
{{-- First / Last name --}}
|
||||
<div class="clu-field-row">
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="firstname" class="text-[11.5px] text-t2 font-medium">Vorname</label>
|
||||
<div class="clu-input">
|
||||
<input wire:model="firstname" id="firstname" name="firstname" type="text"
|
||||
placeholder="Marie" autocomplete="given-name" required autofocus />
|
||||
{{-- Top row --}}
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<div class="flex items-center gap-[10px]">
|
||||
<div class="clu-brand-mark" aria-hidden="true"></div>
|
||||
<div class="font-semibold text-[15px] tracking-[-0.01em]">
|
||||
CluPilot<span class="text-(--color-muted-2) font-medium ml-[5px] text-[11.5px]">v2.4</span>
|
||||
</div>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('firstname')" class="mt-1" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="lastname" class="text-[11.5px] text-t2 font-medium">Nachname</label>
|
||||
<div class="clu-input">
|
||||
<input wire:model="lastname" id="lastname" name="lastname" type="text"
|
||||
placeholder="Weber" autocomplete="family-name" required />
|
||||
<div class="clu-top-link">
|
||||
Schon Konto?
|
||||
<a href="{{ route('login') }}" wire:navigate>Anmelden →</a>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('lastname')" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="reg-email" class="text-[11.5px] text-t2 font-medium">Arbeits-E-Mail</label>
|
||||
<div class="clu-input" id="reg-email-wrap">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>
|
||||
</svg>
|
||||
<input wire:model="email" id="reg-email" name="email" type="email"
|
||||
placeholder="du@firma.de" autocomplete="email" required />
|
||||
<svg class="clu-input-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M5 12l5 5L20 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-1" />
|
||||
{{-- Stepper --}}
|
||||
<div class="clu-stepper" aria-label="Registrierungs-Schritte">
|
||||
<div class="clu-step active">
|
||||
<div class="clu-step-num">1</div>
|
||||
<div class="clu-step-label">Konto</div>
|
||||
</div>
|
||||
<div class="clu-step-line"></div>
|
||||
<div class="clu-step">
|
||||
<div class="clu-step-num">2</div>
|
||||
<div class="clu-step-label">Workspace</div>
|
||||
</div>
|
||||
<div class="clu-step-line"></div>
|
||||
<div class="clu-step">
|
||||
<div class="clu-step-num">3</div>
|
||||
<div class="clu-step-label">Erster Server</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Workspace --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="reg-workspace" class="flex items-center justify-between text-[11.5px] text-t2 font-medium">
|
||||
Workspace-Name
|
||||
<span class="font-normal font-mono text-[10.5px] text-dim">URL für dein Team</span>
|
||||
</label>
|
||||
<div class="clu-input with-suffix">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/>
|
||||
<circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/>
|
||||
</svg>
|
||||
<input wire:model="workspace" id="reg-workspace" name="workspace" type="text"
|
||||
placeholder="Acme Cluster" required />
|
||||
<span class="clu-input-suffix">.clupilot.io</span>
|
||||
</div>
|
||||
<div class="clu-slug-preview">Erreichbar unter <b id="reg-slug-preview">dein-team</b>.clupilot.io</div>
|
||||
<x-input-error :messages="$errors->get('workspace')" class="mt-1" />
|
||||
{{-- Form wrap --}}
|
||||
<div class="my-auto flex flex-col gap-6 w-full" style="max-width:380px;padding:32px 0;">
|
||||
|
||||
<header>
|
||||
<h1 class="m-0 text-[25px] font-semibold tracking-[-0.02em] leading-[1.2]">14 Tage kostenlos testen</h1>
|
||||
<p class="mt-[6px] text-[13.5px] text-(--color-muted)">Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-2 gap-[10px]">
|
||||
<button type="button" class="clu-sso-btn">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/>
|
||||
</svg>
|
||||
Mit Google fortfahren
|
||||
</button>
|
||||
<button type="button" class="clu-sso-btn">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/>
|
||||
</svg>
|
||||
Mit GitHub fortfahren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clu-divider">oder mit E-Mail</div>
|
||||
|
||||
<form wire:submit="register" class="flex flex-col gap-[14px]" autocomplete="on" x-data="{ showPwd: false }">
|
||||
|
||||
{{-- First + Last name --}}
|
||||
<div class="clu-field-row">
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="firstname" class="text-[11.5px] text-(--color-muted) font-semibold">Vorname</label>
|
||||
<div class="clu-input">
|
||||
<input wire:model.live="firstname" id="firstname" name="firstname" type="text"
|
||||
placeholder="Marie" autocomplete="given-name" required />
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('firstname')" class="mt-1" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="lastname" class="text-[11.5px] text-(--color-muted) font-semibold">Nachname</label>
|
||||
<div class="clu-input">
|
||||
<input wire:model.live="lastname" id="lastname" name="lastname" type="text"
|
||||
placeholder="Weber" autocomplete="family-name" required />
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('lastname')" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="email" class="text-[11.5px] text-(--color-muted) font-semibold">Arbeits-E-Mail</label>
|
||||
<div class="clu-input @if($this->emailValid) valid @endif">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/>
|
||||
</svg>
|
||||
<input wire:model.live="email" id="email" name="email" type="email"
|
||||
placeholder="du@firma.de" autocomplete="email" required />
|
||||
<svg class="clu-input-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M5 12l5 5L20 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('email')" class="mt-1" />
|
||||
</div>
|
||||
|
||||
{{-- Workspace --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="workspace" class="flex items-center justify-between text-[11.5px] text-(--color-muted) font-semibold">
|
||||
Workspace-Name
|
||||
<span class="clu-hint">URL für dein Team</span>
|
||||
</label>
|
||||
<div class="clu-input with-suffix">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/>
|
||||
<circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/>
|
||||
</svg>
|
||||
<input wire:model.live="workspace" id="workspace" name="workspace" type="text"
|
||||
placeholder="Acme Cluster" required />
|
||||
<span class="clu-input-suffix">.clupilot.io</span>
|
||||
</div>
|
||||
<div class="clu-slug-preview">Erreichbar unter <b>{{ $this->slug }}</b>.clupilot.io</div>
|
||||
<x-input-error :messages="$errors->get('workspace')" class="mt-1" />
|
||||
</div>
|
||||
|
||||
{{-- Password --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="password" class="flex items-center justify-between text-[11.5px] text-(--color-muted) font-semibold">
|
||||
Passwort
|
||||
<span class="clu-hint">min. 10 Zeichen</span>
|
||||
</label>
|
||||
<div class="clu-input">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
|
||||
</svg>
|
||||
<input wire:model.live="password" id="password" name="password"
|
||||
:type="showPwd ? 'text' : 'password'"
|
||||
placeholder="••••••••••••" autocomplete="new-password" required />
|
||||
<button type="button" class="clu-eye" @click="showPwd = !showPwd"
|
||||
:aria-label="showPwd ? 'Passwort verbergen' : 'Passwort anzeigen'">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="clu-pw-meter @if(strlen($password)) s{{ $this->pwScore }} @endif"><span></span><span></span><span></span><span></span></div>
|
||||
<div class="clu-pw-rules">
|
||||
<span class="clu-pw-rule @if($this->pwChecks['len']) ok @endif"><span class="clu-pw-bullet"></span>10+ Zeichen</span>
|
||||
<span class="clu-pw-rule @if($this->pwChecks['upper']) ok @endif"><span class="clu-pw-bullet"></span>Großbuchstabe</span>
|
||||
<span class="clu-pw-rule @if($this->pwChecks['num']) ok @endif"><span class="clu-pw-bullet"></span>Zahl</span>
|
||||
<span class="clu-pw-rule @if($this->pwChecks['sym']) ok @endif"><span class="clu-pw-bullet"></span>Sonderzeichen</span>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-1" />
|
||||
</div>
|
||||
|
||||
{{-- Terms --}}
|
||||
<label class="clu-checkbox" style="align-items:flex-start;">
|
||||
<input wire:model="terms" type="checkbox" required />
|
||||
<span class="box" style="margin-top:2px;">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12l5 5L20 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="leading-[1.45]">
|
||||
Ich akzeptiere die <a href="#">AGB</a> und stimme der <a href="#">Datenschutzerklärung</a> zu.
|
||||
CluPilot speichert SSH-Keys verschlüsselt in der EU.
|
||||
</span>
|
||||
</label>
|
||||
<x-input-error :messages="$errors->get('terms')" class="-mt-2" />
|
||||
|
||||
{{-- Submit --}}
|
||||
<button class="clu-btn-primary" type="submit" wire:loading.attr="disabled">
|
||||
<svg wire:loading class="clu-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.55"/>
|
||||
</svg>
|
||||
<span wire:loading.remove>Konto erstellen</span>
|
||||
<span wire:loading>Konto wird angelegt…</span>
|
||||
<svg class="arrow" wire:loading.remove width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M5 12h14M13 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Password --}}
|
||||
<div class="flex flex-col gap-[6px]">
|
||||
<label for="reg-password" class="flex items-center justify-between text-[11.5px] text-t2 font-medium">
|
||||
Passwort
|
||||
<span class="font-normal font-mono text-[10.5px] text-dim">min. 10 Zeichen</span>
|
||||
</label>
|
||||
<div class="clu-input">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-dim flex-none" aria-hidden="true">
|
||||
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
|
||||
</svg>
|
||||
<input wire:model="password" id="reg-password" name="password" type="password"
|
||||
placeholder="••••••••••••" autocomplete="new-password" required />
|
||||
<button type="button" id="reg-toggle-eye" aria-label="Passwort anzeigen"
|
||||
class="text-t2 p-[4px] rounded-[4px] grid place-items-center transition-colors hover:text-t1 hover:bg-surface2">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="clu-pw-meter" id="reg-pw-meter"><span></span><span></span><span></span><span></span></div>
|
||||
<div class="clu-pw-rules">
|
||||
<span class="clu-pw-rule" data-rule="len"><span class="clu-pw-bullet"></span>10+ Zeichen</span>
|
||||
<span class="clu-pw-rule" data-rule="upper"><span class="clu-pw-bullet"></span>Großbuchstabe</span>
|
||||
<span class="clu-pw-rule" data-rule="num"><span class="clu-pw-bullet"></span>Zahl</span>
|
||||
<span class="clu-pw-rule" data-rule="sym"><span class="clu-pw-bullet"></span>Sonderzeichen</span>
|
||||
</div>
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-1" />
|
||||
{{-- Footer --}}
|
||||
<footer class="flex items-center justify-between gap-3 text-(--color-muted) text-[11.5px] font-mono pt-[14px] border-t border-(--color-hairline) flex-wrap">
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
|
||||
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
|
||||
</svg>
|
||||
TLS 1.3 · AES-256 · region eu-central-1
|
||||
</div>
|
||||
<div class="flex gap-[14px]">
|
||||
<a href="#" class="text-(--color-muted) hover:text-(--color-fg) no-underline">Datenschutz</a>
|
||||
<a href="#" class="text-(--color-muted) hover:text-(--color-fg) no-underline">AGB</a>
|
||||
<a href="#" class="text-(--color-muted) hover:text-(--color-fg) no-underline">Status</a>
|
||||
<a href="#" class="text-(--color-muted) hover:text-(--color-fg) no-underline">Hilfe</a>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{{-- ============ RIGHT: TRIAL / BENEFITS ============ --}}
|
||||
<aside class="clu-glass clu-auth-side-right flex flex-col gap-[18px]"
|
||||
style="padding:30px 34px;min-height:calc(100vh - 28px);" aria-hidden="true">
|
||||
|
||||
<div class="clu-trial-card">
|
||||
<div class="clu-trial-row">
|
||||
<span class="clu-trial-tag">14 Tage Pro · gratis</span>
|
||||
<span class="clu-trial-count">
|
||||
<span class="clu-pulse-dot text-(--color-success)"></span>
|
||||
47 Teams diese Woche
|
||||
</span>
|
||||
</div>
|
||||
<div class="clu-trial-title">In <em>60 Sekunden</em> deinen ersten Server verbunden.</div>
|
||||
<div class="clu-trial-sub">
|
||||
SSH-Schlüssel hochladen, Site importieren, fertig. Wir kümmern uns ums Monitoring,
|
||||
Backups und Updates — du um dein Business.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Terms --}}
|
||||
<label class="clu-checkbox">
|
||||
<input wire:model="terms" id="reg-terms" type="checkbox" class="sr-only" />
|
||||
<span class="clu-check-box">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12l5 5L20 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>Ich akzeptiere die <a href="#" class="text-akzent font-medium hover:underline">AGB</a> und stimme der <a href="#" class="text-akzent font-medium hover:underline">Datenschutzerklärung</a> zu. CluPilot speichert SSH-Keys verschlüsselt in der EU.</span>
|
||||
</label>
|
||||
<x-input-error :messages="$errors->get('terms')" class="mt-[-6px]" />
|
||||
<ul class="clu-benefit-list">
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 17l5-5-5-5M13 19h7"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">SSH-First Setup</div>
|
||||
<div class="clu-benefit-desc">Verbinde Hetzner, Hostinger oder deinen eigenen Server in 2 Klicks — keine Agents, kein Custom-DNS.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico success">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Stündliche Backups inklusive</div>
|
||||
<div class="clu-benefit-desc">Verschlüsselt nach Frankfurt-S3, 30 Tage Retention, One-Click-Restore — auch für die WP-Datenbank.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico purple">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Bulk-Updates ohne Drama</div>
|
||||
<div class="clu-benefit-desc">WordPress, Plugins und Themes auf 100 Sites gleichzeitig — mit Staging-Diff und automatischem Rollback bei Fehler.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico warn">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Brute-Force-Defense aktiv</div>
|
||||
<div class="clu-benefit-desc">Geo-Fencing, Rate-Limits und WP-User-Lockout — schon im Trial dabei. Letzte 24h: 12.488 Versuche blockiert.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{-- Submit --}}
|
||||
<button class="clu-btn-primary" type="submit" wire:loading.attr="disabled">
|
||||
<svg wire:loading class="clu-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.2-8.55"/>
|
||||
</svg>
|
||||
<span wire:loading.remove>Konto erstellen</span>
|
||||
<span wire:loading>Konto wird angelegt…</span>
|
||||
<svg class="arrow" wire:loading.remove width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path d="M5 12h14M13 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<footer class="flex items-center justify-between gap-3 text-t2 text-[11.5px] font-mono pt-3 border-t border-white/6 flex-wrap">
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true">
|
||||
<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>
|
||||
</svg>
|
||||
TLS 1.3 · AES-256 · region eu-central-1
|
||||
</div>
|
||||
<div class="flex gap-[14px]">
|
||||
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Datenschutz</a>
|
||||
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">AGB</a>
|
||||
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Status</a>
|
||||
<a href="#" class="text-t2 hover:text-t1 no-underline transition-colors">Hilfe</a>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{{-- ===== RIGHT: BENEFITS PANEL ===== --}}
|
||||
<aside class="clu-live-side relative border-l border-white/6 overflow-hidden flex flex-col gap-[22px] min-h-screen"
|
||||
style="background:linear-gradient(180deg,#0c0c12 0%,#0a0a0f 100%);padding:36px 40px;"
|
||||
aria-hidden="true">
|
||||
|
||||
<div class="clu-trial-card">
|
||||
<div class="clu-trial-row">
|
||||
<span class="clu-trial-tag">14 Tage Pro · gratis</span>
|
||||
<span class="clu-trial-count">
|
||||
<span class="clu-pulse-dot w-[6px] h-[6px] rounded-full bg-success flex-none"></span>
|
||||
47 Teams diese Woche
|
||||
</span>
|
||||
</div>
|
||||
<div class="clu-trial-title">In <em>60 Sekunden</em> deinen ersten Server verbunden.</div>
|
||||
<div class="clu-trial-sub">SSH-Schlüssel hochladen, Site importieren, fertig. Wir kümmern uns ums Monitoring, Backups und Updates — du um dein Business.</div>
|
||||
</div>
|
||||
|
||||
<ul class="clu-benefit-list">
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 17l5-5-5-5M13 19h7"/>
|
||||
</svg>
|
||||
<div class="clu-quote-card">
|
||||
<div class="clu-quote-text">
|
||||
Statt 18 SSH-Sessions in Terminal-Tabs habe ich jetzt eine Konsole.
|
||||
Mein Agency-Team hostet 87 Sites — und schläft endlich wieder durch.
|
||||
</div>
|
||||
<div class="clu-quote-author">
|
||||
<div class="clu-avatar">JK</div>
|
||||
<div>
|
||||
<div class="clu-author-name">Julian Koch</div>
|
||||
<div class="clu-author-role">CTO · Helio Digital · 87 Sites</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">SSH-First Setup</div>
|
||||
<div class="clu-benefit-desc">Verbinde Hetzner, Hostinger oder deinen eigenen Server in 2 Klicks — keine Agents, kein Custom-DNS.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico success" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Stündliche Backups inklusive</div>
|
||||
<div class="clu-benefit-desc">Verschlüsselt nach Frankfurt-S3, 30 Tage Retention, One-Click-Restore — auch für die WP-Datenbank.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico purple" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Bulk-Updates ohne Drama</div>
|
||||
<div class="clu-benefit-desc">WordPress, Plugins und Themes auf 100 Sites gleichzeitig — mit Staging-Diff und automatischem Rollback bei Fehler.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="clu-benefit">
|
||||
<div class="clu-benefit-ico warn" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="clu-benefit-title">Brute-Force-Defense aktiv</div>
|
||||
<div class="clu-benefit-desc">Geo-Fencing, Rate-Limits und WP-User-Lockout — schon im Trial dabei. Letzte 24h: 12.488 Versuche blockiert.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="clu-quote-card">
|
||||
<div class="clu-quote-text">Statt 18 SSH-Sessions in Terminal-Tabs habe ich jetzt eine Konsole. Mein Agency-Team hostet 87 Sites — und schläft endlich wieder durch.</div>
|
||||
<div class="clu-quote-author">
|
||||
<div class="clu-avatar">JK</div>
|
||||
<div>
|
||||
<div class="clu-author-name">Julian Koch</div>
|
||||
<div class="clu-author-role">CTO · Helio Digital · 87 Sites</div>
|
||||
<div class="clu-logos">
|
||||
<span>vertrauen uns</span>
|
||||
<span class="clu-logo-pill">helio.</span>
|
||||
<span class="clu-logo-pill">acme▲</span>
|
||||
<span class="clu-logo-pill">opencrate</span>
|
||||
<span class="clu-logo-pill">bergmann</span>
|
||||
<span class="clu-logo-pill">+ 2.1k</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clu-logos">
|
||||
<span>vertrauen uns</span>
|
||||
<span class="clu-logo-pill">helio.</span>
|
||||
<span class="clu-logo-pill">acme▲</span>
|
||||
<span class="clu-logo-pill">opencrate</span>
|
||||
<span class="clu-logo-pill">bergmann</span>
|
||||
<span class="clu-logo-pill">+ 2.1k</span>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use function Pest\Laravel\get;
|
||||
use function Pest\Livewire\livewire;
|
||||
|
||||
it('renders the signup page with template chrome', function () {
|
||||
$response = get(route('register'));
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertSeeText('14 Tage kostenlos testen')
|
||||
->assertSeeText('Keine Kreditkarte')
|
||||
->assertSeeText('Mit Google fortfahren')
|
||||
->assertSeeText('Mit GitHub fortfahren')
|
||||
->assertSeeText('Workspace-Name')
|
||||
->assertSeeText('SSH-First Setup')
|
||||
->assertSeeText('Stündliche Backups inklusive')
|
||||
->assertSeeText('Brute-Force-Defense aktiv')
|
||||
->assertSee('clu-stepper', false)
|
||||
->assertSee('clu-pw-meter', false)
|
||||
->assertSee('clu-trial-card', false)
|
||||
->assertSee('clu-benefit-list', false);
|
||||
});
|
||||
|
||||
it('previews a workspace slug', function () {
|
||||
livewire('pages.auth.register')
|
||||
->set('workspace', 'Acme Cluster GmbH')
|
||||
->assertSeeText('acme-cluster-gmbh');
|
||||
});
|
||||
|
||||
it('rejects an empty form', function () {
|
||||
livewire('pages.auth.register')
|
||||
->set('firstname', '')
|
||||
->set('lastname', '')
|
||||
->set('email', '')
|
||||
->set('workspace', '')
|
||||
->set('password', '')
|
||||
->set('terms', false)
|
||||
->call('register')
|
||||
->assertHasErrors(['firstname', 'lastname', 'email', 'workspace', 'password', 'terms']);
|
||||
});
|
||||
|
||||
it('rejects passwords shorter than 10 chars', function () {
|
||||
livewire('pages.auth.register')
|
||||
->set('firstname', 'Marie')
|
||||
->set('lastname', 'Weber')
|
||||
->set('email', 'marie@example.test')
|
||||
->set('workspace', 'Acme')
|
||||
->set('password', 'short')
|
||||
->set('terms', true)
|
||||
->call('register')
|
||||
->assertHasErrors('password');
|
||||
});
|
||||
|
||||
it('creates a user and logs them in', function () {
|
||||
livewire('pages.auth.register')
|
||||
->set('firstname', 'Marie')
|
||||
->set('lastname', 'Weber')
|
||||
->set('email', 'marie@example.test')
|
||||
->set('workspace', 'Acme Cluster')
|
||||
->set('password', 'longenoughpw!1')
|
||||
->set('terms', true)
|
||||
->call('register')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
expect(User::where('email', 'marie@example.test')->exists())->toBeTrue();
|
||||
expect(auth()->check())->toBeTrue();
|
||||
});
|
||||
|
||||
it('rejects a duplicate email', function () {
|
||||
User::factory()->create(['email' => 'taken@example.test']);
|
||||
|
||||
livewire('pages.auth.register')
|
||||
->set('firstname', 'Marie')
|
||||
->set('lastname', 'Weber')
|
||||
->set('email', 'taken@example.test')
|
||||
->set('workspace', 'Acme')
|
||||
->set('password', 'longenoughpw!1')
|
||||
->set('terms', true)
|
||||
->call('register')
|
||||
->assertHasErrors('email');
|
||||
});
|
||||
Loading…
Reference in New Issue