Add email verification banner for unverified users

Shows a warning banner at the top of dashboard pages when the user's email
is not verified, with a button to resend the verification email.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-10 17:17:43 +08:00
parent d51ee4f219
commit d779aa794c
3 changed files with 81 additions and 0 deletions

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { createQuery } from "@tanstack/svelte-query";
import { api } from "$lib/api/client";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import AppHeader from "./app-header.svelte"; import AppHeader from "./app-header.svelte";
import AppSidebar from "./app-sidebar.svelte"; import AppSidebar from "./app-sidebar.svelte";
import EmailVerificationBanner from "./email-verification-banner.svelte";
interface Props { interface Props {
title: string; title: string;
@@ -11,6 +14,11 @@ interface Props {
} }
let { title, children, class: className }: Props = $props(); let { title, children, class: className }: Props = $props();
const userQuery = createQuery(() => ({
queryKey: ["me"],
queryFn: () => api.me.get(),
}));
</script> </script>
<div class="flex h-screen overflow-hidden bg-background"> <div class="flex h-screen overflow-hidden bg-background">
@@ -20,6 +28,9 @@ let { title, children, class: className }: Props = $props();
</div> </div>
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
{#if userQuery.data && !userQuery.data.emailVerified}
<EmailVerificationBanner email={userQuery.data.email} />
{/if}
<AppHeader {title} /> <AppHeader {title} />
<main class="flex-1 overflow-auto p-4 lg:p-6"> <main class="flex-1 overflow-auto p-4 lg:p-6">

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { Loader2, Mail, RefreshCw } from "@lucide/svelte";
import { api } from "$lib/api/client";
import { Button } from "$lib/components/ui/button";
import { toast } from "svelte-sonner";
interface Props {
email: string;
}
let { email }: Props = $props();
let resendCooldown = $state(0);
let isResending = $state(false);
// Handle cooldown timer
$effect(() => {
if (resendCooldown > 0) {
const timer = setTimeout(() => {
resendCooldown -= 1;
}, 1000);
return () => clearTimeout(timer);
}
});
async function handleResend() {
isResending = true;
try {
await api.auth.resendVerificationEmail();
resendCooldown = 60;
toast.success("Verification email sent!");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to send email");
} finally {
isResending = false;
}
}
</script>
<div
class="flex items-center justify-between gap-4 border-b border-amber-500/30 bg-amber-500/10 px-4 py-3"
>
<div class="flex items-center gap-3">
<Mail class="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
<p class="text-sm text-amber-700 dark:text-amber-300">
Please verify your email address at
<span class="font-medium">{email}</span>
</p>
</div>
<Button
variant="outline"
size="sm"
disabled={resendCooldown > 0 || isResending}
onclick={handleResend}
class="shrink-0 border-amber-500/50 text-amber-700 hover:bg-amber-500/20 dark:text-amber-300"
>
{#if isResending}
<Loader2 class="h-4 w-4 animate-spin" />
Sending...
{:else if resendCooldown > 0}
<RefreshCw class="h-4 w-4" />
Resend ({resendCooldown}s)
{:else}
<RefreshCw class="h-4 w-4" />
Resend verification email
{/if}
</Button>
</div>

View File

@@ -1,4 +1,5 @@
export { default as AppHeader } from "./app-header.svelte"; export { default as AppHeader } from "./app-header.svelte";
export { default as AppSidebar } from "./app-sidebar.svelte"; export { default as AppSidebar } from "./app-sidebar.svelte";
export { default as DashboardLayout } from "./dashboard-layout.svelte"; export { default as DashboardLayout } from "./dashboard-layout.svelte";
export { default as EmailVerificationBanner } from "./email-verification-banner.svelte";
export { default as MobileNav } from "./mobile-nav.svelte"; export { default as MobileNav } from "./mobile-nav.svelte";