Some checks failed
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 3s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 3s
CI/CD Pipeline / VM Test - performance (push) Successful in 2s
CI/CD Pipeline / VM Test - security (push) Successful in 2s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Failing after 12s
CI/CD Pipeline / Nix Flake Check (push) Successful in 37s
CI/CD Pipeline / CI Summary (push) Failing after 0s
175 lines
3.4 KiB
Svelte
175 lines
3.4 KiB
Svelte
<script lang="ts">
|
|
import { createEventDispatcher } from 'svelte';
|
|
|
|
export let isLoading = false;
|
|
|
|
let email = '';
|
|
let password = '';
|
|
let errors: Record<string, string> = {};
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
submit: { email: string; password: string };
|
|
}>();
|
|
|
|
function validateForm(): boolean {
|
|
errors = {};
|
|
|
|
if (!email) {
|
|
errors.email = 'Email is required';
|
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
errors.email = 'Please enter a valid email address';
|
|
}
|
|
|
|
if (!password) {
|
|
errors.password = 'Password is required';
|
|
}
|
|
|
|
return Object.keys(errors).length === 0;
|
|
}
|
|
|
|
function handleSubmit(event: Event) {
|
|
event.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
dispatch('submit', { email, password });
|
|
}
|
|
</script>
|
|
|
|
<form on:submit={handleSubmit} class="login-form">
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
bind:value={email}
|
|
disabled={isLoading}
|
|
placeholder="you@example.com"
|
|
class:error={errors.email}
|
|
autocomplete="email"
|
|
/>
|
|
{#if errors.email}
|
|
<span class="error-text">{errors.email}</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
bind:value={password}
|
|
disabled={isLoading}
|
|
placeholder="••••••••"
|
|
class:error={errors.password}
|
|
autocomplete="current-password"
|
|
/>
|
|
{#if errors.password}
|
|
<span class="error-text">{errors.password}</span>
|
|
{/if}
|
|
</div>
|
|
|
|
<button type="submit" disabled={isLoading} class="submit-button">
|
|
{#if isLoading}
|
|
<span class="spinner"></span>
|
|
Logging in...
|
|
{:else}
|
|
Login
|
|
{/if}
|
|
</button>
|
|
</form>
|
|
|
|
<style>
|
|
.login-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
label {
|
|
font-weight: 500;
|
|
color: #374151;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
input {
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 6px;
|
|
font-size: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
input:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
input.error {
|
|
border-color: #ef4444;
|
|
}
|
|
|
|
input:disabled {
|
|
background-color: #f9fafb;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.error-text {
|
|
color: #ef4444;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.submit-button {
|
|
padding: 0.875rem 1.5rem;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition:
|
|
transform 0.2s,
|
|
box-shadow 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.submit-button:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.submit-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.spinner {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
border-top-color: white;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|