fix: replace flyout profile card with logout slider, i18n auth errors

- Replace static account card in mobile flyout with swipe-to-logout widget
- Remove redundant logout button from flyout bottom
- Make LogoutButton full-width via class prop and dynamic maxSlide
- Extract clean GraphQL error messages instead of raw JSON in all auth forms
- Add i18n keys for known backend errors (invalid credentials, email taken, invalid token)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:26:14 +01:00
parent 0ec27117ae
commit 19d29cbfc6
8 changed files with 29 additions and 54 deletions

View File

@@ -8,8 +8,6 @@
import { getAssetUrl } from "$lib/api"; import { getAssetUrl } from "$lib/api";
import LogoutButton from "../logout-button/logout-button.svelte"; import LogoutButton from "../logout-button/logout-button.svelte";
import Separator from "../ui/separator/separator.svelte"; import Separator from "../ui/separator/separator.svelte";
import { Avatar, AvatarFallback, AvatarImage } from "$lib/components/ui/avatar";
import { getUserInitials } from "$lib/utils";
import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte"; import BurgerMenuButton from "../burger-menu-button/burger-menu-button.svelte";
import Logo from "../logo/logo.svelte"; import Logo from "../logo/logo.svelte";
@@ -180,34 +178,17 @@
</div> </div>
<div class="flex-1 py-6 px-5 space-y-6"> <div class="flex-1 py-6 px-5 space-y-6">
<!-- User Profile Card --> <!-- User logout slider -->
{#if authStatus.authenticated} {#if authStatus.authenticated}
<div <LogoutButton
class="relative overflow-hidden rounded-2xl border border-border/50 bg-gradient-to-br from-card to-card/50 p-4" user={{
> name: authStatus.user!.artist_name || authStatus.user!.email.split("@")[0] || "User",
<div class="absolute inset-0 bg-gradient-to-br from-primary/5 to-accent/5"></div> avatar: getAssetUrl(authStatus.user!.avatar, "mini")!,
<div class="relative flex items-center gap-3"> email: authStatus.user!.email,
<Avatar class="h-9 w-9 ring-2 ring-primary/30"> }}
<AvatarImage onLogout={handleLogout}
src={getAssetUrl(authStatus.user!.avatar, "mini")} class="w-full"
alt={authStatus.user!.artist_name} />
/>
<AvatarFallback
class="bg-gradient-to-br from-primary to-accent text-primary-foreground font-semibold"
>
{getUserInitials(authStatus.user!.artist_name)}
</AvatarFallback>
</Avatar>
<div class="flex flex-1 flex-col min-w-0">
<p class="text-sm font-semibold text-foreground truncate">
{authStatus.user!.artist_name || authStatus.user!.email.split("@")[0]}
</p>
<p class="text-xs text-muted-foreground truncate">
{authStatus.user!.email}
</p>
</div>
</div>
</div>
{/if} {/if}
<!-- Navigation --> <!-- Navigation -->
@@ -340,21 +321,5 @@
</div> </div>
</div> </div>
{#if authStatus.authenticated}
<button
class="cursor-pointer flex w-full items-center gap-3 rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 transition-all duration-200 hover:bg-destructive/10 hover:border-destructive/30 group"
onclick={handleLogout}
>
<div
class="flex h-8 w-8 items-center justify-center rounded-lg bg-destructive/10 group-hover:bg-destructive/20 transition-colors"
>
<span class="icon-[ri--logout-circle-r-line] h-4 w-4 text-destructive"></span>
</div>
<div class="flex flex-1 flex-col gap-0.5 text-left">
<span class="text-sm font-medium text-foreground">{$_("header.logout")}</span>
<span class="text-xs text-muted-foreground">{$_("header.logout_hint")}</span>
</div>
</button>
{/if}
</div> </div>
</div> </div>

View File

@@ -11,15 +11,17 @@
interface Props { interface Props {
user: User; user: User;
onLogout: () => void; onLogout: () => void;
class?: string;
} }
let { user, onLogout }: Props = $props(); let { user, onLogout, class: className = "" }: Props = $props();
let container: HTMLDivElement;
let isDragging = $state(false); let isDragging = $state(false);
let slidePosition = $state(0); let slidePosition = $state(0);
let startX = 0; let startX = 0;
let currentX = 0; let currentX = 0;
let maxSlide = 117; // Maximum slide distance let maxSlide = $derived(container ? container.offsetWidth - 40 : 117);
let threshold = 0.75; // 70% threshold to trigger logout let threshold = 0.75; // 70% threshold to trigger logout
// Calculate slide progress (0 to 1) // Calculate slide progress (0 to 1)
@@ -102,9 +104,10 @@
</script> </script>
<div <div
bind:this={container}
class="relative h-10 w-40 rounded-full bg-muted/30 overflow-hidden select-none transition-all duration-300 bg-muted/40 shadow-lg shadow-accent/10 {isDragging class="relative h-10 w-40 rounded-full bg-muted/30 overflow-hidden select-none transition-all duration-300 bg-muted/40 shadow-lg shadow-accent/10 {isDragging
? 'cursor-grabbing' ? 'cursor-grabbing'
: ''}" : ''} {className}"
style="background: linear-gradient(90deg, style="background: linear-gradient(90deg,
oklch(var(--primary) / 0.3) 0%, oklch(var(--primary) / 0.3) 0%,
oklch(var(--primary) / 0.3) {(1 - slideProgress) * 100}%, oklch(var(--primary) / 0.3) {(1 - slideProgress) * 100}%,

View File

@@ -168,6 +168,7 @@ export default {
no_account: "Don't have an account?", no_account: "Don't have an account?",
sign_up_link: "Sign up now", sign_up_link: "Sign up now",
error: "Heads Up!", error: "Heads Up!",
error_invalid_credentials: "Invalid email or password.",
}, },
signup: { signup: {
title: "Create Account", title: "Create Account",
@@ -194,6 +195,7 @@ export default {
have_account: "Already have an account?", have_account: "Already have an account?",
sign_in_link: "Sign in here", sign_in_link: "Sign in here",
error: "Heads Up!", error: "Heads Up!",
error_email_taken: "This email address is already registered.",
agree_error: "You must confirm our terms of service and your age.", agree_error: "You must confirm our terms of service and your age.",
password_error: "The password has to match the confirmation password.", password_error: "The password has to match the confirmation password.",
toast_register: "A verification email has been sent to {email}!", toast_register: "A verification email has been sent to {email}!",
@@ -221,6 +223,7 @@ export default {
resetting: "Resetting...", resetting: "Resetting...",
reset: "Reset", reset: "Reset",
error: "Heads Up!", error: "Heads Up!",
error_invalid_token: "This reset link is invalid or has expired.",
password_error: "The password has to match the confirmation password.", password_error: "The password has to match the confirmation password.",
toast_reset: "Your password has been reset!", toast_reset: "Your password has been reset!",
}, },

View File

@@ -32,7 +32,8 @@
await login(email, password); await login(email, password);
goto("/videos", { invalidateAll: true }); goto("/videos", { invalidateAll: true });
} catch (err: any) { } catch (err: any) {
error = err.message; const raw = err.response?.errors?.[0]?.message ?? err.message;
error = raw === "Invalid credentials" ? $_("auth.login.error_invalid_credentials") : raw;
isError = true; isError = true;
} finally { } finally {
isLoading = false; isLoading = false;

View File

@@ -87,7 +87,7 @@
toast.success($_("me.settings.toast_update")); toast.success($_("me.settings.toast_update"));
invalidateAll(); invalidateAll();
} catch (err: any) { } catch (err: any) {
profileError = err.message; profileError = err.response?.errors?.[0]?.message ?? err.message;
isProfileError = true; isProfileError = true;
} finally { } finally {
isProfileLoading = false; isProfileLoading = false;
@@ -111,7 +111,7 @@
invalidateAll(); invalidateAll();
password = confirmPassword = ""; password = confirmPassword = "";
} catch (err: any) { } catch (err: any) {
securityError = err.message; securityError = err.response?.errors?.[0]?.message ?? err.message;
isSecurityError = true; isSecurityError = true;
} finally { } finally {
isSecurityLoading = false; isSecurityLoading = false;

View File

@@ -31,7 +31,7 @@
toast.success($_("auth.password_request.toast_request", { values: { email } })); toast.success($_("auth.password_request.toast_request", { values: { email } }));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err: any) {
error = err.message; error = err.response?.errors?.[0]?.message ?? err.message;
isError = true; isError = true;
} finally { } finally {
isLoading = false; isLoading = false;

View File

@@ -40,7 +40,9 @@
toast.success($_("auth.password_reset.toast_reset")); toast.success($_("auth.password_reset.toast_reset"));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err: any) {
error = err.message; const raw = err.response?.errors?.[0]?.message ?? err.message;
const tokenErrors = ["Invalid or expired reset token", "Reset token expired"];
error = tokenErrors.includes(raw) ? $_("auth.password_reset.error_invalid_token") : raw;
isError = true; isError = true;
} finally { } finally {
isLoading = false; isLoading = false;

View File

@@ -48,7 +48,8 @@
toast.success($_("auth.signup.toast_register", { values: { email } })); toast.success($_("auth.signup.toast_register", { values: { email } }));
goto("/login"); goto("/login");
} catch (err: any) { } catch (err: any) {
error = err.message; const raw = err.response?.errors?.[0]?.message ?? err.message;
error = raw === "Email already registered" ? $_("auth.signup.error_email_taken") : raw;
isError = true; isError = true;
} finally { } finally {
isLoading = false; isLoading = false;