diff --git a/AGENTS.md b/AGENTS.md
index c3a8d6b..0cbd7b8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -179,3 +179,13 @@ Check with `claude mcp list`.
- Keep contexts in `/src/lib/contexts/` folder, hooks in `/src/hooks/` folder, and utils in `/src/lib/utils/` folder.
- Extract helper functions with no component dependencies to `/src/lib/utils/` or `/src/lib/` subfolders.
+
+
+
+# This is NOT the Next.js you know
+
+This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
+
+This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
+
+
diff --git a/bun.lock b/bun.lock
index 4bdb033..a7a50de 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,6 +22,7 @@
"better-auth": "1.6.29",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
+ "cmdk": "1.1.1",
"idb-keyval": "6.2.6",
"ioredis": "5.11.1",
"javascript-time-ago": "2.6.4",
@@ -1100,6 +1101,8 @@
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
+ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
+
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="],
diff --git a/package.json b/package.json
index ea353c0..187ccf1 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
"better-auth": "1.6.29",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
+ "cmdk": "1.1.1",
"idb-keyval": "6.2.6",
"ioredis": "5.11.1",
"javascript-time-ago": "2.6.4",
diff --git a/scripts/removers/auth.ts b/scripts/removers/auth.ts
index 77f72f3..7adad59 100644
--- a/scripts/removers/auth.ts
+++ b/scripts/removers/auth.ts
@@ -14,7 +14,6 @@ const AUTH_FILES_TO_DELETE = [
"src/lib/auth-client.ts",
"src/lib/turnstile.ts",
"src/hooks/use-toast.tsx",
- "src/components/top-menu.tsx",
"src/proxy.ts",
"tests/turnstile.test.ts",
];
@@ -33,6 +32,7 @@ const AUTH_FOLDERS_TO_DELETE = [
const AUTH_FILES_TO_MODIFY = [
"src/app/(public)/layout.tsx",
+ "src/components/bottom-navigation.tsx",
"src/lib/config.ts",
"src/lib/public-config.ts",
"src/lib/utils.ts",
diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx
index 9e08bf0..032b3fe 100644
--- a/src/app/(protected)/layout.tsx
+++ b/src/app/(protected)/layout.tsx
@@ -2,10 +2,11 @@ import type { Metadata } from "next";
import type React from "react";
import { Suspense } from "react";
import { SessionGate } from "@/components/auth/session-gate";
+import BottomNavigation from "@/components/bottom-navigation";
+import { CommandPalette } from "@/components/command-palette/command-palette";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
-import { AppSidebarSkeleton } from "@/components/sidebar/app-sidebar-skeleton";
-import TopMenu from "@/components/top-menu";
import { SidebarProvider } from "@/components/ui/sidebar";
+import { CommandPaletteProvider } from "@/lib/contexts/command-palette-context";
export const metadata: Metadata = {
robots: { index: false, follow: false },
@@ -17,19 +18,18 @@ export default function Layout({
children: React.ReactNode;
}>) {
return (
-
-
-
-
- }>
+
+
+
+
+
-
-
-
-
- {children}
-
-
-
+
+ {children}
+
+
+
+
+
);
}
diff --git a/src/app/(protected)/not-found.tsx b/src/app/(protected)/not-found.tsx
new file mode 100644
index 0000000..a6aca55
--- /dev/null
+++ b/src/app/(protected)/not-found.tsx
@@ -0,0 +1,20 @@
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+
+export default function NotFound() {
+ return (
+
+
+
This page is not here
+
+ It may have been deleted, or the link may be out of date. Nothing is broken.
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/settings/page.tsx b/src/app/(protected)/settings/page.tsx
index 3001bda..bce4f0e 100644
--- a/src/app/(protected)/settings/page.tsx
+++ b/src/app/(protected)/settings/page.tsx
@@ -13,40 +13,43 @@ import ApiKeysSkeleton from "@/components/settings/api-keys-skeleton";
// @catalyst:mcp-end
import DeleteAccountForm from "@/components/settings/delete-account-form";
import PasswordForm from "@/components/settings/password-form";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import { userHasPassword } from "@/lib/dao/users";
import { getUserIdFromSession } from "@/lib/session";
export default function SettingsPage() {
return (
-
-
-
- Settings
- Manage your account settings and preferences.
-
-
-
-
}>
-
-
- {/* @catalyst:mcp-start */}
-
- }>
-
-
-
- {/* @catalyst:mcp-end */}
-
-
-
-
+
+
+
Settings
+
+ Change your password, or close your account.
+
+
+
+
+
+ {/* @catalyst:mcp-start */}
+
+ API keys
+
+ }>
+
+
+
+
+ {/* @catalyst:mcp-end */}
+
+
);
}
diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx
index 8363074..302d31d 100644
--- a/src/app/(public)/login/page.tsx
+++ b/src/app/(public)/login/page.tsx
@@ -23,10 +23,10 @@ export const metadata: Metadata = {
export default function Login() {
return (
-
+
+
diff --git a/src/app/(public)/reset-password/page.tsx b/src/app/(public)/reset-password/page.tsx
index 1c2e167..b0b9c04 100644
--- a/src/app/(public)/reset-password/page.tsx
+++ b/src/app/(public)/reset-password/page.tsx
@@ -1,15 +1,9 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
+import AuthCard from "@/components/auth/auth-card";
import { PasswordResetFormSkeleton } from "@/components/auth/password-form-skeleton";
import PasswordResetForm from "@/components/auth/password-reset-form";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import { openGraph } from "@/lib/metadata";
type SearchParams = Promise>;
@@ -36,18 +30,16 @@ export const metadata: Metadata = {
export default function Page({ searchParams }: Readonly<{ searchParams: SearchParams }>) {
return (
-
-
-
- Reset Password
- Enter your new password
-
-
- }>
-
-
-
-
+
+
+ }>
+
+
+
);
}
@@ -67,9 +59,9 @@ async function TokenBoundForm({
function InvalidResetLink() {
return (
- This reset link is invalid or incomplete.{" "}
-
- Request a new one
+ This link is not valid any more.{" "}
+
+ Ask for a new one
.
diff --git a/src/app/(public)/reset-password/request/page.tsx b/src/app/(public)/reset-password/request/page.tsx
index f66450f..413a6fa 100644
--- a/src/app/(public)/reset-password/request/page.tsx
+++ b/src/app/(public)/reset-password/request/page.tsx
@@ -1,12 +1,6 @@
import type { Metadata } from "next";
+import AuthCard from "@/components/auth/auth-card";
import RequestPasswordResetForm from "@/components/auth/request-password-reset-form";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import { openGraph } from "@/lib/metadata";
const path = "/reset-password/request";
@@ -30,16 +24,14 @@ export const metadata: Metadata = {
export default async function Page() {
return (
-
-
-
- Reset Password
- Enter your email to reset your password
-
-
-
-
-
+
+
+
+
);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d64bd1d..f81756a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,4 +1,4 @@
-import type { Metadata } from "next";
+import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import type React from "react";
@@ -20,6 +20,15 @@ import { cn } from "@/lib/utils";
const inter = Inter({ subsets: ["latin"] });
+// Without resizes-content the layout viewport ignores the on-screen keyboard, so
+// bottom-anchored sheets keep their full height behind it and push their fields
+// off the top of the screen.
+export const viewport: Viewport = {
+ width: "device-width",
+ initialScale: 1,
+ interactiveWidget: "resizes-content",
+};
+
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: metaTitle,
diff --git a/src/components/auth/auth-card.tsx b/src/components/auth/auth-card.tsx
index badb0fc..3fccd66 100644
--- a/src/components/auth/auth-card.tsx
+++ b/src/components/auth/auth-card.tsx
@@ -1,14 +1,6 @@
import Link from "next/link";
import type { ReactNode } from "react";
import OAuthForm from "@/components/auth/oauth-form";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import conf from "@/lib/config";
export default function AuthCard({
@@ -18,46 +10,55 @@ export default function AuthCard({
ctaQuestion,
ctaText,
ctaLink,
+ showOAuth = true,
}: {
title: string;
description: string;
children: ReactNode;
- ctaQuestion: string;
- ctaText: string;
- ctaLink: string;
+ ctaQuestion?: string;
+ ctaText?: string;
+ ctaLink?: string;
+ showOAuth?: boolean;
}) {
const hasGitHub = Boolean(conf.githubId) && Boolean(conf.githubSecret);
const hasGoogle = Boolean(conf.googleId) && Boolean(conf.googleSecret);
+ const oauth = showOAuth && (hasGitHub || hasGoogle);
return (
-
-
- {title}
- {description}
-
-
- {children}
- {(hasGitHub || hasGoogle) && (
+
+
+
{title}
+
{description}
+
+
+ {children}
+
+ {oauth && (
+
-
-
Or continue with
+
+ or continue with
- )}
- {hasGoogle &&
}
- {hasGitHub &&
}
-
-
-
+ {hasGoogle && }
+ {hasGitHub && }
+
+ )}
+
+ {ctaQuestion && ctaText && ctaLink && (
+
{ctaQuestion}{" "}
-
+
{ctaText}
-
-
+ )}
+
);
}
diff --git a/src/components/auth/auth-nav.tsx b/src/components/auth/auth-nav.tsx
index 9429c3e..0afd2dc 100644
--- a/src/components/auth/auth-nav.tsx
+++ b/src/components/auth/auth-nav.tsx
@@ -19,14 +19,14 @@ export async function AuthNav() {
return (
<>
-
Login
+
Log in
>
);
}
export function AuthNavSkeleton() {
- return
;
+ return
;
}
diff --git a/src/components/auth/auth-submit-button.tsx b/src/components/auth/auth-submit-button.tsx
new file mode 100644
index 0000000..18c182d
--- /dev/null
+++ b/src/components/auth/auth-submit-button.tsx
@@ -0,0 +1,12 @@
+import LastUsedIndicator from "@/components/auth/last-used-indicator";
+import PendingSubmitButton from "@/components/pending-submit-button";
+
+export default function AuthSubmitButton(
+ props: Readonly
>
+) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx
index 70887e6..60dd1b2 100644
--- a/src/components/auth/login-form.tsx
+++ b/src/components/auth/login-form.tsx
@@ -6,7 +6,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useActionState, useEffect, useId, useRef, useState, useTransition } from "react";
import { useForm } from "react-hook-form";
-import PendingSubmitButton from "@/components/auth/pending-submit-button";
+import AuthSubmitButton from "@/components/auth/auth-submit-button";
import TurnstileComponent from "@/components/auth/turnstile";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -99,7 +99,7 @@ export default function LoginForm() {
href="/reset-password/request"
className="text-primary text-xs underline-offset-4 hover:underline"
>
- Forgot Password?
+ Forgot password?
-
-
-
+
+
);
}
@@ -18,7 +18,7 @@ export function PasswordResetFormSkeleton() {
{RESET_FIELDS.map((field) => (
))}
-
+
);
}
@@ -29,7 +29,7 @@ export function PasswordFormSkeleton() {
{ACCOUNT_FIELDS.map((field) => (
))}
-
+
);
}
diff --git a/src/components/auth/password-reset-form.tsx b/src/components/auth/password-reset-form.tsx
index 33414a9..2410d34 100644
--- a/src/components/auth/password-reset-form.tsx
+++ b/src/components/auth/password-reset-form.tsx
@@ -43,7 +43,7 @@ export default function PasswordResetForm({ token }: { token: string }) {
return (
);
diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx
index a7ddecc..ae52578 100644
--- a/src/components/auth/register-form.tsx
+++ b/src/components/auth/register-form.tsx
@@ -6,7 +6,7 @@ import { ExternalLink } from "lucide-react";
import { useRouter } from "next/navigation";
import { useActionState, useEffect, useId, useRef, useState, useTransition } from "react";
import { useForm } from "react-hook-form";
-import PendingSubmitButton from "@/components/auth/pending-submit-button";
+import AuthSubmitButton from "@/components/auth/auth-submit-button";
import TurnstileComponent from "@/components/auth/turnstile";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -145,7 +145,7 @@ export default function RegisterForm() {
-
- Back to Login
+ Back to log in
diff --git a/src/components/bottom-nav-item.tsx b/src/components/bottom-nav-item.tsx
new file mode 100644
index 0000000..2f30f48
--- /dev/null
+++ b/src/components/bottom-nav-item.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { type ReactNode, Suspense } from "react";
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+type BottomNavItemProps = {
+ href: string;
+ icon: ReactNode;
+ label: string;
+ badge?: ReactNode;
+};
+
+function BottomNavItemView({
+ href,
+ icon,
+ label,
+ badge,
+ active,
+}: BottomNavItemProps & { active: boolean }) {
+ return (
+
+ {active ? (
+
+ ) : null}
+
+ {icon}
+ {badge}
+
+
+ {label}
+
+
+ );
+}
+
+function ActiveBottomNavItem(props: BottomNavItemProps) {
+ const pathname = usePathname();
+ const active = pathname === props.href || pathname.startsWith(`${props.href}/`);
+
+ return ;
+}
+
+export function BottomNavItem(props: BottomNavItemProps) {
+ return (
+ }>
+
+
+ );
+}
diff --git a/src/components/bottom-navigation.tsx b/src/components/bottom-navigation.tsx
new file mode 100644
index 0000000..c46cd94
--- /dev/null
+++ b/src/components/bottom-navigation.tsx
@@ -0,0 +1,76 @@
+import { LayoutDashboard } from "lucide-react";
+import { BottomNavItem } from "./bottom-nav-item";
+
+// @catalyst:auth-start
+
+import { Suspense } from "react";
+import ProfileMenu from "@/components/auth/profile-menu";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { DropdownMenu, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+import { getChromeUser } from "@/lib/session";
+// @catalyst:auth-end
+
+export default function BottomNavigation() {
+ return (
+
+ );
+}
+
+// @catalyst:auth-start
+function ProfileTrigger({ initials, image }: { initials: string; image?: string }) {
+ return (
+
+
+
+ {initials || "\u00b7"}
+
+ You
+
+ );
+}
+
+async function ProfileSlot() {
+ const user = await getChromeUser();
+ if (!user) return ;
+
+ const initials =
+ user.name
+ ?.split(" ")
+ .slice(0, 2)
+ .map((part) => part[0])
+ .join("") || "A";
+
+ return (
+
+
+
+
+
+
+ );
+}
+// @catalyst:auth-end
diff --git a/src/components/command-palette/command-palette.tsx b/src/components/command-palette/command-palette.tsx
new file mode 100644
index 0000000..ec213be
--- /dev/null
+++ b/src/components/command-palette/command-palette.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import { LayoutDashboard, Settings } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useCallback, useEffect } from "react";
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@/components/ui/command";
+import { useCommandPalette } from "@/lib/contexts/command-palette-context";
+
+const PAGES = [
+ { label: "Dashboard", href: "/dashboard", Icon: LayoutDashboard },
+ { label: "Settings", href: "/settings", Icon: Settings },
+];
+
+function isPaletteShortcut(event: KeyboardEvent): boolean {
+ return event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey);
+}
+
+/** @lintignore */
+export function usePaletteNavigation() {
+ const { setOpen } = useCommandPalette();
+ const router = useRouter();
+
+ return useCallback(
+ (href: string) => {
+ setOpen(false);
+ router.push(href);
+ },
+ [router, setOpen]
+ );
+}
+
+export function CommandPalette({ children }: { children?: React.ReactNode }) {
+ const { open, setOpen } = useCommandPalette();
+ const go = usePaletteNavigation();
+
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (!isPaletteShortcut(event)) return;
+ event.preventDefault();
+ setOpen(!open);
+ };
+ document.addEventListener("keydown", onKeyDown);
+ return () => document.removeEventListener("keydown", onKeyDown);
+ }, [open, setOpen]);
+
+ return (
+
+
+
+ Nothing matches.
+
+ {children}
+ {children ? : null}
+
+
+ {PAGES.map(({ label, href, Icon }) => (
+ go(href)}>
+
+ {label}
+
+ ))}
+
+
+
+
+
+ ↑↓ navigate
+
+
+ ↵ open
+
+
+ esc close
+
+
+
+ );
+}
diff --git a/src/components/command-palette/search-trigger.tsx b/src/components/command-palette/search-trigger.tsx
new file mode 100644
index 0000000..b8de4c7
--- /dev/null
+++ b/src/components/command-palette/search-trigger.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { Search } from "lucide-react";
+import { useEffect, useState } from "react";
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar";
+import { useCommandPalette } from "@/lib/contexts/command-palette-context";
+
+function isApplePlatform(): boolean {
+ return /Mac|iPhone|iPad/.test(navigator.platform);
+}
+
+function useShortcutLabel(): string {
+ const [label, setLabel] = useState("");
+ useEffect(() => {
+ setLabel(isApplePlatform() ? "⌘K" : "Ctrl K");
+ }, []);
+ return label;
+}
+
+export function SearchTrigger() {
+ const { setOpen } = useCommandPalette();
+ const shortcut = useShortcutLabel();
+
+ return (
+
+
+
+
+ setOpen(true)}
+ tooltip={`Search · ${shortcut || "⌘K"}`}
+ >
+
+ Search…
+ {shortcut ? (
+
+ {shortcut}
+
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/src/components/notifications/notification-menu-skeleton.tsx b/src/components/notifications/notification-menu-skeleton.tsx
index a774cdc..b02a09f 100644
--- a/src/components/notifications/notification-menu-skeleton.tsx
+++ b/src/components/notifications/notification-menu-skeleton.tsx
@@ -1,5 +1,11 @@
import { Skeleton } from "@/components/ui/skeleton";
-export function NotificationMenuSkeleton() {
- return ;
+export function NotificationListSkeleton() {
+ return (
+
+ {["first", "second", "third"].map((row) => (
+
+ ))}
+
+ );
}
diff --git a/src/components/notifications/notification-menu.tsx b/src/components/notifications/notification-menu.tsx
index ca8b1a5..13f344d 100644
--- a/src/components/notifications/notification-menu.tsx
+++ b/src/components/notifications/notification-menu.tsx
@@ -1,6 +1,8 @@
import { Bell } from "lucide-react";
+import { cache, Suspense } from "react";
import MarkAllAsReadButton from "@/components/notifications/mark-all-as-read-button";
import { NotificationItem } from "@/components/notifications/notification-item";
+import { NotificationListSkeleton } from "@/components/notifications/notification-menu-skeleton";
import { Badge } from "@/components/ui/badge";
import { buttonVariants } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -11,10 +13,13 @@ import type { Notification } from "@/lib/prisma/generated/client";
import { getUserIdFromSession } from "@/lib/session";
import { cn } from "@/lib/utils";
-export default async function NotificationMenu() {
+// Shared by the count and the list so one request reads the table once.
+const notificationsForUser = cache(async () => {
const userId = await getUserIdFromSession();
- const notifications = await getNotifications(userId);
+ return getNotifications(userId);
+});
+export default function NotificationMenu() {
return (
- {notifications.length > 0 && (
-
- {notifications.length > 9 ? "9+" : notifications.length}
-
- )}
+
+
+
Notifications
- {notifications.length > 0 ? (
-
-
-
- {notifications.map((notification: Notification) => (
-
- ))}
-
-
-
-
-
-
-
- ) : (
- No notifications
- )}
+ }>
+
+
);
}
+
+async function UnreadCount() {
+ const notifications = await notificationsForUser();
+
+ if (notifications.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {notifications.length > 9 ? "9+" : notifications.length}
+
+ );
+}
+
+async function NotificationList() {
+ const notifications = await notificationsForUser();
+
+ if (notifications.length === 0) {
+ return No notifications
;
+ }
+
+ return (
+
+
+
+ {notifications.map((notification: Notification) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/auth/pending-submit-button.tsx b/src/components/pending-submit-button.tsx
similarity index 82%
rename from src/components/auth/pending-submit-button.tsx
rename to src/components/pending-submit-button.tsx
index 12d156b..27ecd38 100644
--- a/src/components/auth/pending-submit-button.tsx
+++ b/src/components/pending-submit-button.tsx
@@ -1,5 +1,4 @@
import { LoaderCircle } from "lucide-react";
-import LastUsedIndicator from "@/components/auth/last-used-indicator";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -8,6 +7,7 @@ export default function PendingSubmitButton({
text,
className,
disabled,
+ children,
...props
}: Readonly<
React.ComponentProps & {
@@ -22,7 +22,7 @@ export default function PendingSubmitButton({
{...props}
>
{isPending ? : text}
-
+ {children}
);
}
diff --git a/src/components/settings/delete-account-form.tsx b/src/components/settings/delete-account-form.tsx
index 54a98bc..bd95199 100644
--- a/src/components/settings/delete-account-form.tsx
+++ b/src/components/settings/delete-account-form.tsx
@@ -1,29 +1,33 @@
"use client";
+import { TriangleAlert } from "lucide-react";
import { useRouter } from "next/navigation";
-import { useActionState, useState } from "react";
-import {
- AlertDialog,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
+import { useActionState, useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Modal,
+ ModalContent,
+ ModalDescription,
+ ModalFooter,
+ ModalHeader,
+ ModalTitle,
+ ModalTrigger,
+} from "@/components/ui/responsive-modal";
import useToast from "@/hooks/use-toast";
import { deleteUser } from "@/lib/actions/users";
import { signOut } from "@/lib/auth-client";
import { type FormState, initialState } from "@/lib/utils";
+const CONFIRM_PHRASE = "delete my account";
+
export default function DeleteAccountForm() {
const [state, formAction] = useActionState(deleteUser, initialState);
const [open, setOpen] = useState(false);
const [confirmText, setConfirmText] = useState("");
- const isConfirmed = confirmText === "delete my account";
+ const confirmFieldId = useId();
+ const isConfirmed = confirmText === CONFIRM_PHRASE;
const router = useRouter();
function handleOpenChange(open: boolean) {
@@ -47,46 +51,62 @@ export default function DeleteAccountForm() {
useToast(state, successCallback);
return (
-
-
Delete Account
-
- This action is irreversible. All your data will be permanently deleted.
-
-
-
-
-
-
-
- Are you sure?
-
-
- Warning: This action cannot be undone.
-
-
-
- For security, please type 'delete my account' to proceed:
-
- setConfirmText(e.target.value)}
- placeholder="delete my account"
- />
-
-
-
-
- Cancel
-