Initial Laravel setup

This commit is contained in:
joeplikestocode
2026-02-17 23:30:56 +01:00
commit 09b2b988f7
88 changed files with 15450 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?php
// resources/views/livewire/layout/navbar.blade.php
use Livewire\Component;
new class extends Component
{
public function items(): array
{
return [
['href' => '/dashboard', 'label' => 'Home', 'key' => 'home'],
['href' => '/groups', 'label' => 'Groups', 'key' => 'groups'],
['href' => '/sash', 'label' => 'Sash', 'key' => 'sash'],
['href' => '/profile', 'label' => 'Profile', 'key' => 'profile'],
];
}
public function isActive(string $href): bool
{
$path = ltrim(parse_url($href, PHP_URL_PATH) ?? $href, '/');
if ($path === '') {
return request()->is('/');
}
return request()->is($path) || request()->is($path . '/*');
}
};
?>
<nav
class="fixed bottom-0 left-0 right-0 z-50 border-t border-slate-200 bg-white/95 backdrop-blur"
role="navigation"
aria-label="Main navigation"
>
<div class="mx-auto flex max-w-[430px] items-center justify-around px-2 py-1 pb-[calc(env(safe-area-inset-bottom)+0.25rem)]">
@foreach ($this->items() as $item)
@php($active = $this->isActive($item['href']))
<a
href="{{ $item['href'] }}"
wire:navigate
aria-current="{{ $active ? 'page' : 'false' }}"
class="flex min-h-[44px] min-w-[44px] flex-col items-center justify-center gap-0.5 rounded-lg px-3 py-1.5 text-xs transition-colors
{{ $active ? 'font-semibold text-emerald-700' : 'text-slate-500 active:text-slate-900' }}"
>
@if ($item['key'] === 'home')
<svg class="h-5 w-5 {{ $active ? 'stroke-[2.5]' : '' }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M10 20v-6h4v6m5-8.5L12 4l-7 7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@elseif ($item['key'] === 'groups')
<svg class="h-5 w-5 {{ $active ? 'stroke-[2.5]' : '' }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M16 11a4 4 0 1 0-0.01 0Z" stroke="currentColor" stroke-width="2"/>
<path d="M6 11a3 3 0 1 0-0.01 0Z" stroke="currentColor" stroke-width="2"/>
<path d="M2.5 20a6 6 0 0 1 7.5-4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M10 20a7 7 0 0 1 14 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
@elseif ($item['key'] === 'sash')
<svg class="h-5 w-5 {{ $active ? 'stroke-[2.5]' : '' }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2l7 4v6c0 5-3 9-7 10-4-1-7-5-7-10V6l7-4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
@elseif ($item['key'] === 'profile')
<svg class="h-5 w-5 {{ $active ? 'stroke-[2.5]' : '' }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 12a4 4 0 1 0-0.01 0Z" stroke="currentColor" stroke-width="2"/>
<path d="M4 21a8 8 0 0 1 16 0" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
@endif
<span>{{ $item['label'] }}</span>
</a>
@endforeach
</div>
</nav>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ $title ?? config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="min-h-dvh bg-background text-foreground">
<div class="mx-auto min-h-dvh w-full max-w-[430px] bg-background px-4 pt-4 pb-[calc(env(safe-area-inset-bottom)+1rem)]">
<main class="@auth pb-24 @else pb-6 @endauth">
{{ $slot }}
</main>
@auth
<livewire:layout.navbar />
@endauth
</div>
@livewireScripts
</body>
</html>

View File

@@ -0,0 +1,100 @@
@component('layouts.app', ['title' => 'Login'])
<div class="min-h-dvh flex items-center justify-center p-6">
<div class="w-full max-w-md rounded-2xl bg-card p-6 ring-1 ring-border">
<div class="text-center">
<div class="mx-auto flex items-center justify-center gap-2">
<div class="grid h-12 w-12 place-items-center rounded-xl bg-primary text-primary-foreground shadow-sm">
<svg class="h-7 w-7" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2l7 4v6c0 5-3 9-7 10-4-1-7-5-7-10V6l7-4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<span class="text-2xl font-bold tracking-tight">PatchBook</span>
</div>
<h1 class="mt-4 text-2xl font-bold tracking-tight">Welcome back</h1>
<p class="mt-2 text-sm text-muted-foreground">Log in to your crew and continue earning patches.</p>
</div>
<div class="mt-6">
<a href="{{ route('workos.redirect') }}" class="btn-outline w-full text-base">
<span class="inline-flex items-center justify-center gap-2">
<svg class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M21.35 11.1H12v2.9h5.35c-.25 1.45-1.65 4.25-5.35 4.25-3.2 0-5.8-2.65-5.8-5.9s2.6-5.9 5.8-5.9c1.85 0 3.1.8 3.8 1.5l2.6-2.5C16.8 3.95 14.6 3 12 3 6.9 3 2.75 7.15 2.75 12.35S6.9 21.7 12 21.7c5.6 0 9.3-3.95 9.3-9.5 0-.65-.1-1.1-.2-1.1Z"/>
</svg>
<span>Continue with Google</span>
</span>
</a>
<div class="my-6 flex items-center gap-3">
<hr class="flex-1 border-border" />
<span class="text-xs text-muted-foreground">or</span>
<hr class="flex-1 border-border" />
</div>
<form method="POST" action="{{ route('login') }}" class="space-y-4">
@csrf
<div>
<label for="email" class="block text-sm font-medium">Email</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
required
autofocus
autocomplete="email"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('email')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-medium">Password</label>
<input
id="password"
name="password"
type="password"
required
autocomplete="current-password"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('password')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center justify-between">
<label class="inline-flex items-center gap-2 text-sm text-muted-foreground">
<input
type="checkbox"
name="remember"
class="h-4 w-4 rounded border-border text-primary focus:ring-ring/30"
/>
Remember me
</label>
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="text-sm font-semibold text-primary">
Forgot password?
</a>
@endif
</div>
<button type="submit" class="btn-primary w-full text-base">
Log in
</button>
</form>
<p class="mt-5 text-center text-sm text-muted-foreground">
No account yet?
<a href="{{ route('register') }}" class="font-semibold text-primary" wire:navigate>
Sign up
</a>
</p>
</div>
</div>
</div>
@endcomponent

View File

@@ -0,0 +1,138 @@
<?php
use Livewire\Component;
new class extends Component
{
//
};
?>
<div class="px-6 pt-16 pb-10 text-center">
<div>
<div class="mx-auto flex items-center justify-center gap-2">
<div class="grid h-12 w-12 place-items-center rounded-xl bg-primary text-primary-foreground shadow-sm">
<svg class="h-7 w-7" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2l7 4v6c0 5-3 9-7 10-4-1-7-5-7-10V6l7-4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<span class="text-2xl font-bold tracking-tight">PatchBook</span>
</div>
<h1 class="mt-6 text-3xl font-bold leading-tight tracking-tight text-balance">
Earn patches. <span class="text-primary">Challenge friends.</span> Build your sash.
</h1>
<p class="mx-auto mt-4 max-w-[320px] text-sm leading-relaxed text-muted-foreground">
Create fun challenges with your crew, earn badges, track progress, and see who tops the leaderboard.
</p>
<div class="mx-auto mt-8 flex w-full max-w-[360px] flex-col gap-3">
<a href="{{ route('register') }}" wire:navigate class="btn-primary w-full text-base">
Get Started
</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn-primary w-full text-base">
Logout
</button>
</form>
<a href="{{ route('login') }}" wire:navigate class="btn-outline w-full text-base">
I already have an account
</a>
</div>
</div>
<hr class="mt-5 mb-5 border-t border-muted-foreground opacity-20">
<div class="text-left">
<h2 class="text-2xl mb-5"><b>How it works</b></h2>
<div>
<div class="flex items-start gap-4">
<div class="relative w-10 flex justify-center self-stretch">
<div class="h-10 w-10 rounded-full bg-primary text-primary-foreground grid place-items-center shrink-0">
<x-bi-people class="h-6 w-6" />
</div>
<div class="absolute top-12 bottom-0 w-px bg-muted-foreground/20"></div>
</div>
<div>
<p class="text-primary text-sm">STEP 1</p>
<h3 class="font-semibold">Start or join a group</h3>
<p class="text-muted-foreground text-sm">Create a crew with friends, family, or coworkers. Share an invite code to get everyone on board.</p>
</div>
</div>
</div>
<div>
<div class="flex items-start gap-4 mt-5">
<div class="relative w-10 flex justify-center self-stretch">
<div class="h-10 w-10 rounded-full bg-primary text-primary-foreground grid place-items-center shrink-0">
<x-solar-medal-ribbon-linear class="h-6 w-6" />
</div>
<div class="absolute top-12 bottom-0 w-px bg-muted-foreground/20"></div>
</div>
<div>
<p class="text-primary text-sm">STEP 2</p>
<h3 class="font-semibold">Design your patches</h3>
<p class="text-muted-foreground text-sm">Dream up custom challenges with icons, colors, and step-by-step requirements to complete.</p>
</div>
</div>
</div>
<div>
<div class="flex items-start gap-4 mt-5">
<div class="relative w-10 flex justify-center self-stretch">
<div class="h-10 w-10 rounded-full bg-primary text-primary-foreground grid place-items-center shrink-0">
<x-solar-shield-minimalistic-linear class="h-6 w-6" />
</div>
<div class="absolute top-12 bottom-0 w-px bg-muted-foreground/20"></div>
</div>
<div>
<p class="text-primary text-sm">STEP 3</p>
<h3 class="font-semibold">Earn and collect</h3>
<p class="text-muted-foreground text-sm">Complete challenges, get verified by your crew, and watch your sash fill up with patches.</p>
</div>
</div>
</div>
</div>
<hr class="mt-5 mb-5 border-t border-muted-foreground opacity-20">
<div class="text-left">
<h2 class="text-2xl mb-5"><b>Built for your crew</b></h2>
<div class="grid grid-cols-2 gap-4">
<div class="aspect-square rounded-2xl bg-card ring-1 ring-border p-4 text-sm">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-primary/15 text-primary-foreground shadow-sm mb-2">
<x-bi-people class="h-6 w-6 text-primary" />
</div>
<b>Groups</b>
<p class="text-muted-foreground">Private crews with invite codes</p>
</div>
<div class="aspect-square rounded-2xl bg-card ring-1 ring-border p-4 text-sm">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-primary/15 text-primary-foreground shadow-sm mb-2">
<x-bi-people class="h-6 w-6 text-primary" />
</div>
<b>Patches</b>
<p class="text-muted-foreground">Private crews with invite codes</p>
</div>
<div class="aspect-square rounded-2xl bg-card ring-1 ring-border p-4 text-sm">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-primary/15 text-primary-foreground shadow-sm mb-2">
<x-bi-people class="h-6 w-6 text-primary" />
</div>
<b>Your sash</b>
<p class="text-muted-foreground">Private crews with invite codes</p>
</div>
<div class="aspect-square rounded-2xl bg-card ring-1 ring-border p-4 text-sm">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-primary/15 text-primary-foreground shadow-sm mb-2">
<x-feathericon-target class="h-6 w-6 text-primary" />
</div>
<b>Progress</b>
<p class="text-muted-foreground">Private crews with invite codes</p>
</div>
</div>
</div>
<p class="mt-8 text-xs text-muted-foreground">
Made for fun, inspired by scouting.
</p>
</div>

View File

@@ -0,0 +1,120 @@
@component('layouts.app', ['title' => 'Register'])
<div class="min-h-dvh flex items-center justify-center p-6">
<div class="w-full max-w-md rounded-2xl bg-card p-6 ring-1 ring-border">
<div class="text-center">
<div class="mx-auto flex items-center justify-center gap-2">
<div class="grid h-12 w-12 place-items-center rounded-xl bg-primary text-primary-foreground shadow-sm">
<svg class="h-7 w-7" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2l7 4v6c0 5-3 9-7 10-4-1-7-5-7-10V6l7-4Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>
</div>
<span class="text-2xl font-bold tracking-tight">PatchBook</span>
</div>
<h1 class="mt-4 text-2xl font-bold tracking-tight">Create your account</h1>
<p class="mt-2 text-sm text-muted-foreground">Join a crew, earn patches, build your sash.</p>
</div>
<div class="mt-6">
<a
href="{{ route('workos.redirect') }}"
class="btn-outline w-full text-base"
>
<span class="inline-flex items-center justify-center gap-2">
<svg class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M21.35 11.1H12v2.9h5.35c-.25 1.45-1.65 4.25-5.35 4.25-3.2 0-5.8-2.65-5.8-5.9s2.6-5.9 5.8-5.9c1.85 0 3.1.8 3.8 1.5l2.6-2.5C16.8 3.95 14.6 3 12 3 6.9 3 2.75 7.15 2.75 12.35S6.9 21.7 12 21.7c5.6 0 9.3-3.95 9.3-9.5 0-.65-.1-1.1-.2-1.1Z"/>
</svg>
<span>Continue with Google</span>
</span>
</a>
<div class="my-6 flex items-center gap-3">
<hr class="flex-1 border-border" />
<span class="text-xs text-muted-foreground">or</span>
<hr class="flex-1 border-border" />
</div>
<form method="POST" action="{{ route('register') }}" class="space-y-4">
@csrf
<div>
<label for="name" class="block text-sm font-medium">Display name</label>
<input
id="name"
name="name"
type="text"
value="{{ old('name') }}"
required
autofocus
autocomplete="name"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('name')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="block text-sm font-medium">Email</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
required
autocomplete="email"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('email')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-medium">Password</label>
<input
id="password"
name="password"
type="password"
required
autocomplete="new-password"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('password')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password_confirmation" class="block text-sm font-medium">
Confirm Password
</label>
<input
id="password_confirmation"
name="password_confirmation"
type="password"
required
autocomplete="new-password"
class="mt-1 w-full rounded-xl bg-background px-4 py-3 text-sm ring-1 ring-border focus:outline-none focus:ring-2 focus:ring-ring/30"
/>
@error('password_confirmation')
<p class="mt-1 text-xs text-destructive">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="btn-primary w-full text-base">
Create account
</button>
</form>
<p class="mt-5 text-center text-sm text-muted-foreground">
Already have an account?
<a href="{{ route('login') }}" class="font-semibold text-primary" wire:navigate>
Log in
</a>
</p>
</div>
</div>
</div>
@endcomponent