diff --git a/app/resources/css/app.css b/app/resources/css/app.css index 24856b6..df210d6 100644 --- a/app/resources/css/app.css +++ b/app/resources/css/app.css @@ -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; } } diff --git a/app/resources/views/livewire/pages/auth/register.blade.php b/app/resources/views/livewire/pages/auth/register.blade.php index 430161b..c5646fe 100644 --- a/app/resources/views/livewire/pages/auth/register.blade.php +++ b/app/resources/views/livewire/pages/auth/register.blade.php @@ -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 } }; ?> -
+
- {{-- ===== LEFT: FORM ===== --}} -
+ {{-- ============ LEFT: FORM ============ --}} +
- {{-- Top row --}} -
-
- -
- CluPilotv2.4 -
-
- -
- - {{-- Stepper --}} - - - {{-- Form wrap --}} -
- -
-

14 Tage kostenlos testen

-

Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.

-
- - {{-- SSO --}} -
- - -
- -
oder mit E-Mail
- - {{-- Form --}} -
- - {{-- First / Last name --}} -
-
- -
- + {{-- Top row --}} +
+
+ +
+ CluPilotv2.4 +
- -
-
- -
- + - -
- {{-- Email --}} -
- -
- - - -
- + {{-- Stepper --}} +
+
+
1
+
Konto
+
+
+
+
2
+
Workspace
+
+
+
+
3
+
Erster Server
+
- {{-- Workspace --}} -
- -
- - - .clupilot.io -
-
Erreichbar unter dein-team.clupilot.io
- + {{-- Form wrap --}} +
+ +
+

14 Tage kostenlos testen

+

Keine Kreditkarte. Volle Funktionen. Jederzeit kündbar.

+
+ +
+ + +
+ +
oder mit E-Mail
+ + + + {{-- First + Last name --}} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + {{-- Email --}} +
+ +
+ + + +
+ +
+ + {{-- Workspace --}} +
+ +
+ + + .clupilot.io +
+
Erreichbar unter {{ $this->slug }}.clupilot.io
+ +
+ + {{-- Password --}} +
+ +
+ + + +
+
+
+ 10+ Zeichen + Großbuchstabe + Zahl + Sonderzeichen +
+ +
+ + {{-- Terms --}} + + + + {{-- Submit --}} + +
- {{-- Password --}} -
- -
- - - -
-
-
- 10+ Zeichen - Großbuchstabe - Zahl - Sonderzeichen -
- + {{-- Footer --}} + +
+ + {{-- ============ RIGHT: TRIAL / BENEFITS ============ --}} +
- - {{-- Footer --}} - - - - {{-- ===== RIGHT: BENEFITS PANEL ===== --}} - - +
diff --git a/app/tests/Feature/Auth/SignupPageTest.php b/app/tests/Feature/Auth/SignupPageTest.php new file mode 100644 index 0000000..58aeb96 --- /dev/null +++ b/app/tests/Feature/Auth/SignupPageTest.php @@ -0,0 +1,83 @@ +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'); +});