Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Supabase configuration
# Replace with your actual Supabase project URL and anon key
VITE_SUPABASE_URL=https://your-project-id.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.49.4",
"@vitest/coverage-v8": "3.0.8",
"dexie": "^4.0.11"
},
Expand Down
212 changes: 171 additions & 41 deletions app/pnpm-lock.yaml

Large diffs are not rendered by default.

257 changes: 257 additions & 0 deletions app/src/lib/components/AuthDropdown.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<script lang="ts">
import { user } from "$lib/supabase/client";
import {
loginWithEmail,
registerWithEmail,
logout,
} from "$lib/supabase/auth";

let isOpen = $state(false);
let isRegistering = $state(false);
let email = $state("");
let password = $state("");
let confirmPassword = $state("");
let errorMessage = $state("");
let successMessage = $state("");

// Toggle dropdown visibility
function toggleDropdown() {
isOpen = !isOpen;
clearMessages();
resetForm();
}

// Toggle between login and registration forms
function toggleAuthMode() {
isRegistering = !isRegistering;
clearMessages();
resetForm();
}

// Reset form fields
function resetForm() {
email = "";
password = "";
confirmPassword = "";
}

// Clear status messages
function clearMessages() {
errorMessage = "";
successMessage = "";
}

// Handle login submission
async function handleLogin() {
clearMessages();

try {
const { success, error } = await loginWithEmail(email, password);

if (success) {
successMessage = "Login successful!";
isOpen = false;
} else {
errorMessage = error?.message || "Login failed. Please try again.";
}
} catch (err) {
errorMessage = "An unexpected error occurred. Please try again.";
console.error("Login error:", err);
}
}

// Handle registration submission
async function handleRegister() {
clearMessages();

if (password !== confirmPassword) {
errorMessage = "Passwords do not match.";
return;
}

try {
const { success, error } = await registerWithEmail(email, password);

if (success) {
successMessage =
"Registration successful! Please check your email for verification.";
isRegistering = false;
} else {
errorMessage =
error?.message || "Registration failed. Please try again.";
}
} catch (err) {
errorMessage = "An unexpected error occurred. Please try again.";
console.error("Registration error:", err);
}
}

// Handle logout
async function handleLogout() {
try {
await logout();
successMessage = "Logged out successfully!";
} catch (err) {
errorMessage = "Error logging out. Please try again.";
console.error("Logout error:", err);
}
}
</script>

<div class="dropdown dropdown-end">
{#if $user}
<!-- Logged in state -->
<button
onclick={toggleDropdown}
class="btn btn-ghost flex items-center"
id="auth-user-button"
>
<span class="mr-1">{$user.email}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>

{#if isOpen}
<ul
class="dropdown-content menu bg-base-100 z-10 mt-2 w-48 rounded-md shadow-lg"
id="auth-dropdown-menu"
>
<li>
<button
onclick={handleLogout}
class="w-full text-left"
id="auth-logout-button"
>
Logout
</button>
</li>
</ul>
{/if}
{:else}
<!-- Logged out state -->
<button
onclick={toggleDropdown}
class="btn btn-ghost flex items-center"
id="auth-login-button"
>
<span class="mr-1">Login</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>

{#if isOpen}
<div
class="dropdown-content card bg-base-100 z-10 mt-2 w-64 rounded-md shadow-lg"
id="auth-form-container"
>
<div class="card-body p-4">
<!-- Form title -->
<h3 class="card-title text-lg">
{isRegistering ? "Register" : "Login"}
</h3>

<!-- Error/Success messages -->
{#if errorMessage}
<div class="alert alert-error py-2" id="auth-error-message">
<span>{errorMessage}</span>
</div>
{/if}

{#if successMessage}
<div class="alert alert-success py-2" id="auth-success-message">
<span>{successMessage}</span>
</div>
{/if}

<!-- Auth Form -->
<form class="form-control gap-3">
<div>
<label for="email" class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
id="auth-email-input"
bind:value={email}
class="input input-bordered w-full"
required
/>
</div>

<div>
<label for="password" class="label">
<span class="label-text">Password</span>
</label>
<input
type="password"
id="auth-password-input"
bind:value={password}
class="input input-bordered w-full"
required
/>
</div>

{#if isRegistering}
<div>
<label for="confirmPassword" class="label">
<span class="label-text">Confirm Password</span>
</label>
<input
type="password"
id="auth-confirm-password-input"
bind:value={confirmPassword}
class="input input-bordered w-full"
required
/>
</div>
{/if}

<div>
<button
type="button"
onclick={isRegistering ? handleRegister : handleLogin}
class="btn btn-primary mt-2 w-full"
id="auth-submit-button"
>
{isRegistering ? "Register" : "Login"}
</button>
</div>

<div class="mt-2 text-center text-xs">
<button
type="button"
onclick={toggleAuthMode}
class="btn btn-link btn-sm p-0"
id="auth-toggle-mode-button"
>
{isRegistering
? "Already have an account? Login"
: "Need an account? Register"}
</button>
</div>
</form>
</div>
</div>
{/if}
{/if}
</div>
34 changes: 22 additions & 12 deletions app/src/lib/components/NavigationMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,70 +1,80 @@
<script lang="ts">
import { page } from "$app/stores";
import { page } from "$app/state";
import { base } from "$app/paths";
import AuthDropdown from "$lib/components/AuthDropdown.svelte";

// Track active route to highlight current page
$: pathname = $page.url.pathname;
let pathname = $derived(page.url.pathname);
</script>

<nav class="bg-gray-800 p-4">
<nav class="navbar bg-neutral text-neutral-content p-4">
<div class="container mx-auto flex items-center justify-between">
<a href="{base}/" class="text-xl font-bold text-white">Workouts</a>
<a href="{base}/" class="text-xl font-bold">Workouts</a>

<div class="flex gap-4">
<div class="flex items-center gap-4">
<a
href="{base}/"
class="text-white transition-colors hover:text-blue-300 {pathname ===
base + '/'
class="hover:text-primary transition-colors {pathname === base + '/'
? 'font-bold'
: ''}"
id="nav-home"
>
Home
</a>
<a
href="{base}/exercises"
class="text-white transition-colors hover:text-blue-300 {pathname ===
class="hover:text-primary transition-colors {pathname ===
base + '/exercises'
? 'font-bold'
: ''}"
id="nav-exercises"
>
Exercises
</a>
<a
href="{base}/workout"
class="text-white transition-colors hover:text-blue-300 {pathname ===
class="hover:text-primary transition-colors {pathname ===
base + '/workout'
? 'font-bold'
: ''}"
id="nav-workout"
>
Generate Workout
</a>
<a
href="{base}/history"
class="text-white transition-colors hover:text-blue-300 {pathname ===
class="hover:text-primary transition-colors {pathname ===
base + '/history'
? 'font-bold'
: ''}"
id="nav-history"
>
History
</a>
<a
href="{base}/recovery"
class="text-white transition-colors hover:text-blue-300 {pathname ===
class="hover:text-primary transition-colors {pathname ===
base + '/recovery'
? 'font-bold'
: ''}"
id="nav-recovery"
>
Recovery
</a>
<a
href="{base}/guidelines"
class="text-white transition-colors hover:text-blue-300 {pathname ===
class="hover:text-primary transition-colors {pathname ===
base + '/guidelines'
? 'font-bold'
: ''}"
id="nav-guidelines"
>
Guidelines
</a>

<!-- Add Authentication Dropdown -->
<div class="divider divider-horizontal mx-2"></div>
<AuthDropdown />
</div>
</div>
</nav>
Loading
Loading