mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on the profile module.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, {ReactNode} from "react";
|
||||||
import {redirect} from "next/navigation";
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
||||||
@@ -6,21 +6,19 @@ import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sid
|
|||||||
import {Header} from "@/features/layout/Header";
|
import {Header} from "@/features/layout/Header";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default async function Layout({children}: { children: ReactNode }) {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SidebarProvider>
|
||||||
<SidebarProvider>
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<AppSidebar/>
|
||||||
<AppSidebar/>
|
<SidebarInset>
|
||||||
<SidebarInset>
|
<Header/>
|
||||||
<Header/>
|
<main className="h-full">{children}</main>
|
||||||
<main className="h-full">{children}</main>
|
</SidebarInset>
|
||||||
</SidebarInset>
|
</div>
|
||||||
</div>
|
</SidebarProvider>
|
||||||
</SidebarProvider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -5,6 +5,7 @@ import {Providers} from "./providers";
|
|||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||||
import {inter} from "@/fonts/fonts";
|
import {inter} from "@/fonts/fonts";
|
||||||
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
|
|
||||||
const title = process.env.PROJECT_NAME ?? "App Title";
|
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||||
|
|
||||||
@@ -16,19 +17,23 @@ export const metadata: Metadata = {
|
|||||||
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
|
const user = await currentUser();
|
||||||
|
console.info("useerrrrr",user)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
<meta name="apple-mobile-web-app-title" content={title}/>
|
||||||
</head>
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body className={cn(inter.className, "h-full")}>
|
||||||
<ConsoleSilencer/>
|
<ConsoleSilencer/>
|
||||||
<Providers>{children}</Providers>
|
<Providers >{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
+10
-3
@@ -5,14 +5,21 @@ import { ThemeProvider } from "@/features/theme/theme-provider";
|
|||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{
|
||||||
|
}>;
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null} >
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
>
|
||||||
|
<ThemeMetaUpdater />
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/l
|
|||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const projectName = env.PROJECT_NAME;
|
const projectName = env.PROJECT_NAME;
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon" >
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -24,6 +24,7 @@ import z from "zod";
|
|||||||
import {zPassword} from "@/lib/zod";
|
import {zPassword} from "@/lib/zod";
|
||||||
import {BackupCodesList} from "../components/backup-codes-list";
|
import {BackupCodesList} from "../components/backup-codes-list";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const PasswordSchema = z.object({
|
const PasswordSchema = z.object({
|
||||||
password: zPassword(),
|
password: zPassword(),
|
||||||
@@ -34,9 +35,10 @@ type Password = z.infer<typeof PasswordSchema>;
|
|||||||
type Setup2FAModalProps = {
|
type Setup2FAModalProps = {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModalProps) {
|
export function Setup2FAProfileProviderModal({onOpenChange, open, disabled}: Setup2FAModalProps) {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [step, setStep] = useState<"PASSWORD" | "QR" | "BACKUP">("PASSWORD");
|
const [step, setStep] = useState<"PASSWORD" | "QR" | "BACKUP">("PASSWORD");
|
||||||
@@ -113,10 +115,9 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => (!v ? handleClose() : onOpenChange(v))}>
|
<Dialog open={open} onOpenChange={(v) => (!v ? handleClose() : onOpenChange(v))}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild disabled={disabled}>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<ShieldCheck className="w-4 h-4 mr-2"/>
|
<ShieldCheck className="w-4 h-4 mr-2"/>
|
||||||
Enable Two-Factor
|
Enable Two-Factor
|
||||||
@@ -177,7 +178,8 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-2">
|
<div className="w-full space-y-2">
|
||||||
<p className="text-xs text-muted-foreground text-center">If you are unable to scan the QR code, you can manually enter the secret key into your authentication app :</p>
|
<p className="text-xs text-muted-foreground text-center">If you are unable to scan the
|
||||||
|
QR code, you can manually enter the secret key into your authentication app :</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code
|
<code
|
||||||
className="flex-1 bg-muted p-2 rounded text-xs font-mono break-all text-center">{secret}</code>
|
className="flex-1 bg-muted p-2 rounded text-xs font-mono break-all text-center">{secret}</code>
|
||||||
@@ -237,7 +239,8 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
|||||||
className="border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-900">
|
className="border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-900">
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400"/>
|
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400"/>
|
||||||
<AlertDescription
|
<AlertDescription
|
||||||
className="text-green-700 dark:text-green-400">Two Factor Authentication is now enabled on your account.</AlertDescription>
|
className="text-green-700 dark:text-green-400">Two Factor Authentication is now enabled
|
||||||
|
on your account.</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<BackupCodesList codes={backupCodes}/>
|
<BackupCodesList codes={backupCodes}/>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function ThemeSelector() {
|
|||||||
)}
|
)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// setTheme(item.value)
|
// setTheme(item.value)
|
||||||
// await authClient.updateUser({theme: item.value});
|
await authClient.updateUser({theme: item.value});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -74,11 +74,9 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="font-medium">Password</div>
|
<div className="font-medium">Password</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{/*{user.lastChangedPasswordAt*/}
|
{user.lastChangedPasswordAt
|
||||||
{/* ? t("sections.authentication.password.description.last_changed", {*/}
|
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
|
||||||
{/* date: timeAgo(new Date(user.lastChangedPasswordAt), locale),*/}
|
: "Never changed"}
|
||||||
{/* })*/}
|
|
||||||
{/* : "Never changed"}*/}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{credentialAccount ? (
|
{credentialAccount ? (
|
||||||
@@ -116,8 +114,11 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
|
|||||||
onOpenChange={setIsDisable2FADialogOpen}/>
|
onOpenChange={setIsDisable2FADialogOpen}/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Setup2FAProfileProviderModal open={isSetup2FADialogOpen}
|
<Setup2FAProfileProviderModal
|
||||||
onOpenChange={setIsSetup2FADialogOpen}/>
|
disabled={!credentialAccount}
|
||||||
|
open={isSetup2FADialogOpen}
|
||||||
|
onOpenChange={setIsSetup2FADialogOpen}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE "passkey" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text,
|
||||||
|
"publicKey" text NOT NULL,
|
||||||
|
"userId" uuid NOT NULL,
|
||||||
|
"credentialId" text NOT NULL,
|
||||||
|
"counter" integer NOT NULL,
|
||||||
|
"deviceType" text NOT NULL,
|
||||||
|
"backedUp" boolean NOT NULL,
|
||||||
|
"transports" text,
|
||||||
|
"updated_at" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "two_factor" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"secret" text NOT NULL,
|
||||||
|
"backup_codes" text NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TYPE "public"."user_themes" AS ENUM('light', 'dark', 'system');--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD COLUMN "theme" "user_themes" DEFAULT 'light' NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,20 @@
|
|||||||
"when": 1766327760039,
|
"when": 1766327760039,
|
||||||
"tag": "0013_past_logan",
|
"tag": "0013_past_logan",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1766424611387,
|
||||||
|
"tag": "0014_strong_galactus",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1766426190521,
|
||||||
|
"tag": "0015_absurd_next_avengers",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {relations} from "drizzle-orm";
|
import {relations} from "drizzle-orm";
|
||||||
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
||||||
import {createSelectSchema} from "drizzle-zod";
|
import {createSelectSchema} from "drizzle-zod";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {project} from "./06_project";
|
import {project} from "./06_project";
|
||||||
@@ -9,6 +9,9 @@ import {organization} from "@/db/schema/03_organization";
|
|||||||
import {Account as BetterAuthAccount} from "better-auth";
|
import {Account as BetterAuthAccount} from "better-auth";
|
||||||
import {timestamps} from "@/db/schema/00_common";
|
import {timestamps} from "@/db/schema/00_common";
|
||||||
|
|
||||||
|
export const userThemeEnum = pgEnum("user_themes", ["light", "dark", "system"]);
|
||||||
|
|
||||||
|
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
id: uuid("id").defaultRandom().primaryKey(),
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
@@ -16,6 +19,7 @@ export const user = pgTable("user", {
|
|||||||
emailVerified: boolean("email_verified").notNull(),
|
emailVerified: boolean("email_verified").notNull(),
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
role: text("role"),
|
role: text("role"),
|
||||||
|
theme: userThemeEnum().notNull().default("light"),
|
||||||
banned: boolean("banned"),
|
banned: boolean("banned"),
|
||||||
banReason: text("ban_reason"),
|
banReason: text("ban_reason"),
|
||||||
banExpires: timestamp("ban_expires"),
|
banExpires: timestamp("ban_expires"),
|
||||||
@@ -66,6 +70,31 @@ export const verification = pgTable("verification", {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const passkey = pgTable("passkey", {
|
||||||
|
id: uuid().defaultRandom().primaryKey(),
|
||||||
|
name: text(),
|
||||||
|
publicKey: text().notNull(),
|
||||||
|
userId: uuid()
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, {onDelete: "cascade"}),
|
||||||
|
credentialId: text().notNull(),
|
||||||
|
counter: integer().notNull(),
|
||||||
|
deviceType: text().notNull(),
|
||||||
|
backedUp: boolean().notNull(),
|
||||||
|
transports: text(),
|
||||||
|
|
||||||
|
...timestamps,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const twoFactor = pgTable("two_factor", {
|
||||||
|
id: uuid().defaultRandom().primaryKey(),
|
||||||
|
secret: text("secret").notNull(),
|
||||||
|
backupCodes: text("backup_codes").notNull(),
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, {onDelete: "cascade"}),
|
||||||
|
});
|
||||||
|
|
||||||
export const userRelations = relations(user, ({many}) => ({
|
export const userRelations = relations(user, ({many}) => ({
|
||||||
sessions: many(session),
|
sessions: many(session),
|
||||||
accounts: many(account),
|
accounts: many(account),
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {useTheme} from "next-themes";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
|
export function ThemeMetaUpdater() {
|
||||||
|
const {resolvedTheme, setTheme} = useTheme();
|
||||||
|
const {data: session} = authClient.useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resolvedTheme) return;
|
||||||
|
|
||||||
|
const color = resolvedTheme === "dark" ? "#000000" : "#ffffff";
|
||||||
|
|
||||||
|
let tag = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
||||||
|
if (!tag) {
|
||||||
|
tag = document.createElement("meta");
|
||||||
|
tag.setAttribute("name", "theme-color");
|
||||||
|
document.head.appendChild(tag);
|
||||||
|
}
|
||||||
|
tag.setAttribute("content", color);
|
||||||
|
}, [resolvedTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session?.user?.theme) return;
|
||||||
|
setTheme(session.user.theme);
|
||||||
|
}, [session, resolvedTheme, setTheme]);
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// "use client"
|
||||||
|
// import {useEffect, useState} from "react";
|
||||||
|
// import {useTheme} from "next-themes";
|
||||||
|
// import {authClient} from "@/lib/auth/auth-client";
|
||||||
|
//
|
||||||
|
// export function ThemeMetaUpdater() {
|
||||||
|
// const [mounted, setMounted] = useState(false);
|
||||||
|
// const {resolvedTheme, setTheme} = useTheme();
|
||||||
|
// const {data: session} = authClient.useSession();
|
||||||
|
//
|
||||||
|
// useEffect(() => setMounted(true), []);
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!mounted || !resolvedTheme) return;
|
||||||
|
// const color = resolvedTheme === "dark" ? "#000000" : "#ffffff";
|
||||||
|
// let tag = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
||||||
|
// if (!tag) {
|
||||||
|
// tag = document.createElement("meta");
|
||||||
|
// tag.setAttribute("name", "theme-color");
|
||||||
|
// document.head.appendChild(tag);
|
||||||
|
// }
|
||||||
|
// tag.setAttribute("content", color);
|
||||||
|
// }, [mounted, resolvedTheme]);
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!mounted || !session?.user?.theme) return;
|
||||||
|
// if (resolvedTheme !== session.user.theme) {
|
||||||
|
// setTheme(session.user.theme);
|
||||||
|
// }
|
||||||
|
// }, [mounted, session, setTheme]);
|
||||||
|
//
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
import {SidebarTrigger} from "@/components/ui/sidebar";
|
import {SidebarTrigger} from "@/components/ui/sidebar";
|
||||||
import {ModeToggle} from "@/features/theme/ModeToggle";
|
import {ModeToggle} from "@/features/theme/mode-toggle";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
|
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
|
||||||
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
|
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {Moon, Sun} from "lucide-react"
|
import {Moon, Sun} from "lucide-react"
|
||||||
import {useTheme} from "next-themes"
|
|
||||||
|
|
||||||
import {Button} from "@/components/ui/button"
|
import {Button} from "@/components/ui/button"
|
||||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
||||||
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const {setTheme} = useTheme()
|
|
||||||
|
const setTheme = async (theme: "light" | "dark" | "system") => {
|
||||||
|
toast.success("Your theme preference has been updated.");
|
||||||
|
await authClient.updateUser({theme: theme});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -33,3 +37,5 @@ export function ModeToggle() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ export const auth = betterAuth({
|
|||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema: drizzleDb.schemas,
|
schema: drizzleDb.schemas,
|
||||||
}),
|
}),
|
||||||
|
appName: env.PROJECT_NAME!,
|
||||||
baseURL: env.PROJECT_URL,
|
baseURL: env.PROJECT_URL,
|
||||||
secret: env.PROJECT_SECRET,
|
secret: env.PROJECT_SECRET,
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
@@ -131,6 +132,15 @@ export const auth = betterAuth({
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
lastConnectedAt: {
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
|
lastChangedPasswordAt: {
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
databaseHooks: {
|
databaseHooks: {
|
||||||
|
|||||||
Reference in New Issue
Block a user