Files
edh-stats/frontend/src/routes/register/+page.svelte
Michael d69a14d80b Migrate frontend to SvelteKit with comprehensive deployment (#2)
* Migrate frontend to SvelteKit with comprehensive deployment
documentation

- Create new SvelteKit project structure with routing, stores, and
  components
- Implement complete authentication system with auth store and protected
  routes
- Build all application pages: Home, Login, Register, Dashboard, Games,
  Stats, Commanders, Profile, and Round Counter
- Configure Vite, TailwindCSS, PostCSS, and Nginx for production
  deployment
- Add Dockerfile.svelte for containerized builds with multi-stage
  optimization
- Create comprehensive SVELTE_DEPLOYMENT.md and SVELTE_MIGRATION.md
  guides
- Update deployment scripts and package dependencies for SvelteKit
  ecosystem

* feat: Add user authentication and game tracking pages to EDH Stats
Tracker

* Migrate frontend to SvelteKit and update Docker configuration

- Replace Alpine.js with SvelteKit for improved DX and hot module
  replacement
- Switch frontend service to `Dockerfile.dev` with volume mounts and
  Vite dev server
- Update `docker-compose.yml` to map ports 5173 and use
  `http://localhost:5173` for CORS
- Add `Dockerfile.svelte` for production static builds
- Configure Vite proxy to target `http://backend:3000` in containers and
  `localhost:3002` locally
- Migrate existing components to new routes and update authentication
  store logic
- Add Chart.js integration to stats page and handle field name mapping
  for forms
- Include static assets (`fonts/Beleren-Bold.ttf`) and update deployment
  scripts
- Document migration status, testing checklist, and known minor issues
  in guides

* Refactor frontend state properties from snake_case to camelCase

This commit standardizes frontend property access across Dashboard,
Games, and Stats pages.
Changes include:
- Renaming API data fields (e.g., `commanderName`, `playerCount`,
  `winRate`).
- Updating `startEdit` logic to normalize mixed snake_case/camelCase
  inputs.
- Replacing template literals like `_player_won` with camelCase
  versions.
- Consistent usage of `totalGames` and `wins` instead of snake_case
  variants.

* Update version to 2.1.12 and refactor commander management

- Upgrade application version to 2.1.12
- Add Footer component and include in all pages
- Refactor `/commanders` page to fetch commanders and stats separately
- Fix commander API endpoint to load all commanders instead of only
  those with stats
- Add stats merging logic to calculate wins, win rate, and avg rounds
- Split add/edit command logic into shared `loadCommanders` function
- Fix color toggle logic to work with both new and editing command modes
- Update API methods for update requests to send `PUT` for existing
  commanders
- Enhance commander delete functionality with proper API response
  handling
- Refactor dashboard and stats pages to reuse shared data loading logic
- Add chart cleanup on destroy for both dashboard and stats pages
- Implement Chart.js for Win Rate by Color and Player Count charts
- Reorganize round counter component state and timer functions
- Add localStorage persistence for round counter with pause/resume
  support
- Update game log page to integrate footer component

* Refactor auth store and backend to use stable user ID

*   Backend: Switch user lookup from username to ID in auth routes to
    maintain stability across username changes.
*   Frontend: Update user store to reflect ID-based updates.
*   UI: Refactor user menu Svelte component to use ID-based user data.
*   Profile: Switch profile page to use ID-based user data for
    validation and state management.

* format date formatting options consistently across dashboard and games
pages

* format date formatting options consistently across dashboard and games
pages

* Refactor card action buttons to use icons with semantic text

- Switch "Edit" and "Delete" button text to SVG icons in `commanders`
  and `games` pages
- Update icon colors and font styles to match standard design tokens
  (indigo/red, bold text)
- Improve responsive spacing by adding `lg:grid-cols-3`

* grids
- Clarify hover states and titles for better UX accessibility
  Bump application versions to 2.2.0 and update deployment configuration

* Convert `+page.svelte` to use template strings for multiline strings and
fix syntax errors.

* Update static version to 2.2.2 and tighten nginx cache headers
2026-04-11 10:42:46 +02:00

389 lines
12 KiB
Svelte

<script>
import { auth } from '$stores/auth';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
let formData = {
username: '',
email: '',
password: '',
confirmPassword: '',
terms: false
};
let errors = {};
let showPassword = false;
let showConfirmPassword = false;
let loading = false;
let serverError = '';
let successMessage = '';
let allowRegistration = true;
onMount(async () => {
await auth.checkRegistrationConfig();
auth.subscribe(($auth) => {
allowRegistration = $auth.allowRegistration;
});
});
function validateUsername() {
if (!formData.username.trim()) {
errors.username = 'Username is required';
} else if (formData.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
} else if (formData.username.length > 50) {
errors.username = 'Username must be less than 50 characters';
} else if (!/^[a-zA-Z0-9_-]+$/.test(formData.username)) {
errors.username = 'Username can only contain letters, numbers, underscores, and hyphens';
} else {
errors.username = '';
}
}
function validateEmail() {
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
errors.email = 'Please enter a valid email address';
} else {
errors.email = '';
}
}
function validatePassword() {
if (!formData.password) {
errors.password = 'Password is required';
} else if (formData.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
} else if (formData.password.length > 100) {
errors.password = 'Password must be less than 100 characters';
} else if (!/(?=.*[a-z])/.test(formData.password)) {
errors.password = 'Password must contain at least one lowercase letter';
} else if (!/(?=.*[A-Z])/.test(formData.password)) {
errors.password = 'Password must contain at least one uppercase letter';
} else if (!/(?=.*\d)/.test(formData.password)) {
errors.password = 'Password must contain at least one number';
} else {
errors.password = '';
}
}
function validateConfirmPassword() {
if (!formData.confirmPassword) {
errors.confirmPassword = 'Please confirm your password';
} else if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
} else {
errors.confirmPassword = '';
}
}
function validateTerms() {
if (!formData.terms) {
errors.terms = 'You must agree to the Terms of Service';
} else {
errors.terms = '';
}
}
async function handleRegister(e) {
e.preventDefault();
// Validate all fields
validateUsername();
validateEmail();
validatePassword();
validateConfirmPassword();
validateTerms();
if (Object.values(errors).some((error) => error)) {
return;
}
loading = true;
serverError = '';
const result = await auth.register(formData.username, formData.email, formData.password);
if (result.success) {
successMessage = 'Account created successfully! Redirecting...';
setTimeout(() => {
goto('/dashboard');
}, 1000);
} else {
serverError = result.error;
}
loading = false;
}
</script>
<svelte:head>
<title>Register - EDH Stats Tracker</title>
<meta
name="description"
content="Create an account to track your Magic: The Gathering EDH/Commander games"
/>
</svelte:head>
<div class="min-h-full flex flex-col py-12 px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-center flex-1">
<div class="max-w-md w-full space-y-8">
<!-- Header -->
<div class="text-center">
<h1 class="text-4xl font-bold font-mtg text-edh-primary mb-2">EDH Stats</h1>
<h2 class="text-xl text-gray-600">Create your account</h2>
</div>
{#if !allowRegistration}
<div class="card">
<div class="text-center py-8">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
></path>
</svg>
<h3 class="mt-2 text-lg font-medium text-gray-900">Registration Closed</h3>
<p class="mt-1 text-sm text-gray-500">
New user registration is currently disabled.
</p>
<div class="mt-6">
<a href="/login" class="btn btn-primary"> Go to Login </a>
</div>
</div>
</div>
{:else}
<!-- Registration Form -->
<div class="card">
<form class="space-y-6" on:submit={handleRegister}>
<!-- Username -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
Username *
</label>
<input
id="username"
type="text"
required
bind:value={formData.username}
on:input={validateUsername}
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 {errors.username
? 'border-red-500'
: ''}"
placeholder="Choose a username"
/>
{#if errors.username}
<p class="mt-1 text-sm text-red-600">{errors.username}</p>
{/if}
</div>
<!-- Email (Optional) -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
Email (optional)
</label>
<input
id="email"
type="email"
bind:value={formData.email}
on:input={validateEmail}
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 {errors.email
? 'border-red-500'
: ''}"
placeholder="your.email@example.com"
/>
{#if errors.email}
<p class="mt-1 text-sm text-red-600">{errors.email}</p>
{/if}
</div>
<!-- Password -->
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
Password *
</label>
<div class="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
required
bind:value={formData.password}
on:input={validatePassword}
class="appearance-none block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 {errors.password
? 'border-red-500'
: ''}"
placeholder="Create a strong password"
/>
<button
type="button"
on:click={() => (showPassword = !showPassword)}
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{#if showPassword}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
{/if}
</svg>
</button>
</div>
{#if errors.password}
<p class="mt-1 text-sm text-red-600">{errors.password}</p>
{/if}
</div>
<!-- Confirm Password -->
<div>
<label
for="confirmPassword"
class="block text-sm font-medium text-gray-700 mb-1"
>
Confirm Password *
</label>
<div class="relative">
<input
id="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
required
bind:value={formData.confirmPassword}
on:input={validateConfirmPassword}
class="appearance-none block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 {errors.confirmPassword
? 'border-red-500'
: ''}"
placeholder="Confirm your password"
/>
<button
type="button"
on:click={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
class="h-5 w-5 text-gray-400 hover:text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{#if showConfirmPassword}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
></path>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
{/if}
</svg>
</button>
</div>
{#if errors.confirmPassword}
<p class="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
{/if}
</div>
<!-- Terms -->
<div>
<div class="flex items-start">
<input
id="terms"
type="checkbox"
bind:checked={formData.terms}
on:change={validateTerms}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded mt-1"
/>
<label for="terms" class="ml-2 block text-sm text-gray-900">
I agree to the Terms of Service and Privacy Policy
</label>
</div>
{#if errors.terms}
<p class="mt-1 text-sm text-red-600">{errors.terms}</p>
{/if}
</div>
<!-- Success Message -->
{#if successMessage}
<div class="rounded-md bg-green-50 p-4">
<p class="text-sm font-medium text-green-800">{successMessage}</p>
</div>
{/if}
<!-- Server Error -->
{#if serverError}
<div class="rounded-md bg-red-50 p-4">
<p class="text-sm font-medium text-red-800">{serverError}</p>
</div>
{/if}
<!-- Submit -->
<button
type="submit"
disabled={loading}
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{#if loading}
<div class="loading-spinner w-5 h-5"></div>
{:else}
Create Account
{/if}
</button>
</form>
<!-- Links -->
<div class="mt-6 text-center space-y-2">
<p class="text-sm text-gray-600">
Already have an account?
<a href="/login" class="font-medium text-indigo-600 hover:text-indigo-500">
Sign in
</a>
</p>
<p class="text-sm text-gray-600">
<a href="/" class="font-medium text-indigo-600 hover:text-indigo-500">
← Back to Home
</a>
</p>
</div>
</div>
{/if}
</div>
</div>
</div>