mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
update: ui
This commit is contained in:
@@ -6,7 +6,6 @@ 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 {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
||||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
|
||||||
|
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Label} from "@/components/ui/label";
|
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Copy, Check} from "lucide-react";
|
import {Copy, Check, Eye, EyeOff, Terminal, Key, Info} from "lucide-react";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
|
import {copyToClipboardWithMeta} from "@/components/wrappers/common/button/copy-button";
|
||||||
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type AgentCardKeyProps = {
|
export type AgentCardKeyProps = {
|
||||||
edgeKey: string;
|
edgeKey: string;
|
||||||
@@ -15,74 +17,199 @@ export type AgentCardKeyProps = {
|
|||||||
export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
|
export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
|
||||||
const [isCopiedKey, setIsCopiedKey] = useState(false);
|
const [isCopiedKey, setIsCopiedKey] = useState(false);
|
||||||
const [isCopiedCommand, setIsCopiedCommand] = useState(false);
|
const [isCopiedCommand, setIsCopiedCommand] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
const command = `portabase agent "${agentName}" --key ${edgeKey}`;
|
const command = `portabase agent "${agentName}" --key ${edgeKey}`;
|
||||||
|
const maskedKey = "••••••••••••••••••••••••••••••••";
|
||||||
|
|
||||||
const handleCopy = async (text: string, setter: (v: boolean) => void) => {
|
const handleCopy = async (text: string, setter: (v: boolean) => void) => {
|
||||||
await copyToClipboardWithMeta(text);
|
await copyToClipboardWithMeta(text);
|
||||||
setter(true);
|
setter(true);
|
||||||
setTimeout(() => setter(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
event.target.select();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 py-2">
|
<div className="flex flex-col gap-4 py-2">
|
||||||
<div className="space-y-4">
|
<Tabs defaultValue="automatic" className="w-full">
|
||||||
<div className="flex flex-col gap-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<Label className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
<TabsTrigger value="automatic" className="gap-2">
|
||||||
1. Registration Key
|
<Terminal className="h-4 w-4" />
|
||||||
</Label>
|
<span>Automatic</span>
|
||||||
<div className="flex items-center gap-2">
|
</TabsTrigger>
|
||||||
<Input
|
<TabsTrigger value="manual" className="gap-2">
|
||||||
readOnly
|
<Key className="h-4 w-4" />
|
||||||
value={edgeKey}
|
<span>Manual</span>
|
||||||
onFocus={handleFocus}
|
</TabsTrigger>
|
||||||
className="font-mono text-xs bg-muted/30 focus-visible:ring-1 cursor-pointer"
|
</TabsList>
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleCopy(edgeKey, setIsCopiedKey)}
|
|
||||||
className="shrink-0"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{isCopiedKey ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] text-muted-foreground">
|
|
||||||
Use this key for manual configuration of your agent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 pt-2">
|
<TabsContent value="automatic" className="mt-4 focus-visible:outline-none">
|
||||||
<Label className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
|
<Card className="border-muted/60 shadow-none py-0">
|
||||||
2. Automatic Setup (CLI)
|
<CardHeader className="pb-3 px-4 pt-4">
|
||||||
</Label>
|
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
|
||||||
<div className="flex items-center gap-2">
|
CLI Setup
|
||||||
<Input
|
</CardTitle>
|
||||||
readOnly
|
<CardDescription className="text-xs leading-relaxed">
|
||||||
value={command}
|
Click the input below to reveal the key and copy it to your terminal.
|
||||||
onFocus={handleFocus}
|
</CardDescription>
|
||||||
className="font-mono text-xs bg-muted/30 focus-visible:ring-1 cursor-pointer"
|
</CardHeader>
|
||||||
/>
|
<CardContent className="px-4 pb-4 space-y-4">
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="outline"
|
<div className="relative flex-1">
|
||||||
size="icon"
|
<Input
|
||||||
onClick={() => handleCopy(command, setIsCopiedCommand)}
|
readOnly
|
||||||
className="shrink-0"
|
value={isVisible ? command : `portabase agent "${agentName}" --key ${maskedKey}`}
|
||||||
type="button"
|
onFocus={(e) => {
|
||||||
>
|
setIsVisible(true);
|
||||||
{isCopiedCommand ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
handleCopy(command, setIsCopiedCommand);
|
||||||
</Button>
|
e.currentTarget.select();
|
||||||
</div>
|
}}
|
||||||
<p className="text-[11px] text-muted-foreground">
|
onClick={(e) => e.currentTarget.select()}
|
||||||
Run this command on your server to automatically register the agent.
|
onBlur={() => {
|
||||||
</p>
|
setIsVisible(false);
|
||||||
</div>
|
setIsCopiedCommand(false);
|
||||||
</div>
|
}}
|
||||||
|
className="font-mono text-xs bg-muted/30 h-10 pr-10 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsVisible(!isVisible);
|
||||||
|
}}
|
||||||
|
className="absolute right-1 top-1.5 h-7 w-7"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCopy(command, setIsCopiedCommand)}
|
||||||
|
className="shrink-0 gap-2 h-10 px-4"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isCopiedCommand ? (
|
||||||
|
<><Check className="h-4 w-4 text-green-500" /></>
|
||||||
|
) : (
|
||||||
|
<><Copy className="h-4 w-4" /></>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-md bg-orange-500/10 dark:bg-orange-500/5 border border-orange-500/20">
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<Terminal className="h-4 w-4 text-orange-600 dark:text-orange-400 shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-bold text-orange-900 dark:text-orange-100">CLI Installation</p>
|
||||||
|
<p className="text-[11px] text-orange-800/80 dark:text-orange-400/80 leading-normal">
|
||||||
|
This command requires the <strong>portabase cli</strong>. If you haven't installed it yet, you can find the instructions at{" "}
|
||||||
|
<a
|
||||||
|
href="https://portabase.io/docs/cli"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-bold underline underline-offset-2 hover:text-orange-700 dark:hover:text-orange-300 transition-colors"
|
||||||
|
>
|
||||||
|
portabase.io/docs/cli
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="manual" className="mt-4 focus-visible:outline-none">
|
||||||
|
<Card className="border-muted/60 shadow-none py-0">
|
||||||
|
<CardHeader className="pb-3 px-4 pt-4">
|
||||||
|
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
|
||||||
|
Registration Key
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs leading-relaxed">
|
||||||
|
Use this key to manually configure your agent in your configuration file.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-4 pb-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
readOnly
|
||||||
|
value={isVisible ? edgeKey : maskedKey}
|
||||||
|
onFocus={(e) => {
|
||||||
|
setIsVisible(true);
|
||||||
|
handleCopy(edgeKey, setIsCopiedKey);
|
||||||
|
e.currentTarget.select();
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.currentTarget.select()}
|
||||||
|
onBlur={() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setIsCopiedKey(false);
|
||||||
|
}}
|
||||||
|
className="font-mono text-xs bg-muted/30 h-10 pr-10 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsVisible(!isVisible);
|
||||||
|
}}
|
||||||
|
className="absolute right-1 top-1.5 h-7 w-7 hover:bg-transparent"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isVisible ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleCopy(edgeKey, setIsCopiedKey)}
|
||||||
|
className="shrink-0 gap-2 h-10 px-4 "
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isCopiedKey ? (
|
||||||
|
<><Check className="h-4 w-4 text-green-500" /></>
|
||||||
|
) : (
|
||||||
|
<><Copy className="h-4 w-4" /></>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="p-3 rounded-md bg-orange-500/10 dark:bg-orange-500/5 border border-orange-500/20">
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<Info className="h-4 w-4 text-orange-600 dark:text-orange-400 shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="text-xs font-bold text-orange-900 dark:text-orange-100">Important Security Note</p>
|
||||||
|
<p className="text-[11px] text-orange-800/80 dark:text-orange-400/80 leading-normal">
|
||||||
|
Never share this key. Anyone with access to it can register as this agent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 rounded-md bg-muted/30 border border-muted/50">
|
||||||
|
<div className="flex gap-2.5">
|
||||||
|
<Terminal className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-bold text-foreground">Need help with manual setup?</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
||||||
|
Check out our guide for manual configuration and Docker deployment at{" "}
|
||||||
|
<a
|
||||||
|
href="https://portabase.io/docs/agent/setup#docker"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-bold underline underline-offset-2 hover:text-orange-500 transition-colors text-foreground"
|
||||||
|
>
|
||||||
|
portabase.io/docs/setup
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,36 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {PropsWithChildren, ReactNode, useState} from "react";
|
import {PropsWithChildren, ReactNode, useState} from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { CircleUser, LogOut } from "lucide-react";
|
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { LogOut, User } from "lucide-react";
|
||||||
|
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
import { signOut } from "@/lib/auth/auth-client";
|
import { signOut } from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profile-modal";
|
import {ProfileModal} from "@/components/wrappers/dashboard/common/profile/profile-modal";
|
||||||
import {Account, Session, User} from "@/db/schema/02_user";
|
|
||||||
|
import {Account, Session, User as UserType} from "@/db/schema/02_user";
|
||||||
|
|
||||||
import {AuthProviderConfig} from "../../../../../../portabase.config";
|
import {AuthProviderConfig} from "../../../../../../portabase.config";
|
||||||
|
|
||||||
export type LoggedInDropdownProps = PropsWithChildren<{
|
export type LoggedInDropdownProps = PropsWithChildren<{
|
||||||
user: User;
|
user: UserType;
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
currentSession: Session;
|
currentSession: Session;
|
||||||
accounts: Account[];
|
accounts: Account[];
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
providers: AuthProviderConfig[]
|
providers: AuthProviderConfig[]
|
||||||
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children, providers }: LoggedInDropdownProps) => {
|
export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, children, providers }: LoggedInDropdownProps) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,34 +44,57 @@ export const LoggedInDropdown = ({ user, sessions, currentSession, accounts, chi
|
|||||||
onOpenChange={setIsModalOpen}
|
onOpenChange={setIsModalOpen}
|
||||||
providers={providers}
|
providers={providers}
|
||||||
/>
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
<DropdownMenuContent
|
||||||
<DropdownMenuContent side="top" className="min-w-[var(--radix-popper-anchor-width)]">
|
className="min-w-[220px] rounded-xl border-2 border-border bg-popover shadow-none p-1"
|
||||||
<DropdownMenuItem onClick={() => setIsModalOpen(!isModalOpen)}>
|
align="start"
|
||||||
<div className="flex justify-start items-center gap-2">
|
side="top"
|
||||||
<CircleUser size={16} />
|
sideOffset={8}
|
||||||
<span>Account</span>
|
>
|
||||||
</div>
|
<div className="flex flex-col space-y-1 p-2 mb-1">
|
||||||
</DropdownMenuItem>
|
<p className="text-sm font-semibold leading-none truncate">{user.name}</p>
|
||||||
<DropdownMenuItem
|
<p className="text-xs leading-none text-muted-foreground truncate">
|
||||||
onClick={async () => {
|
{user.email}
|
||||||
await signOut({
|
</p>
|
||||||
fetchOptions: {
|
</div>
|
||||||
onSuccess: () => {
|
<DropdownMenuSeparator className="-mx-1 mb-1 bg-border/50" />
|
||||||
router.push("/login");
|
<DropdownMenuItem
|
||||||
},
|
onClick={() => setIsModalOpen(!isModalOpen)}
|
||||||
},
|
className="group gap-2 p-1 cursor-pointer rounded-lg mb-1 transition-colors focus:bg-accent hover:bg-accent/50 border border-transparent"
|
||||||
});
|
>
|
||||||
}}
|
<div className="flex size-9 items-center justify-center rounded-md border border-border bg-muted/50 shadow-sm transition-all group-hover:shadow-md group-hover:bg-background">
|
||||||
>
|
<User size={18} className="text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
<div className="flex justify-start items-center gap-2">
|
</div>
|
||||||
<LogOut size={16} className="text-red-500" />
|
<div className="flex flex-col">
|
||||||
<span className="text-red-500">Logout</span>
|
<span className="text-sm font-medium leading-none">Account Settings</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuItem
|
||||||
</DropdownMenu>
|
className="group gap-2 p-1 cursor-pointer rounded-lg transition-colors focus:bg-red-50 dark:focus:bg-red-950/20 border border-transparent text-red-600 focus:text-red-600"
|
||||||
|
onClick={async () => {
|
||||||
|
await signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/login");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex size-9 items-center justify-center rounded-md border border-red-100 bg-red-50/50 dark:border-red-900/30 dark:bg-red-950/20 shadow-sm transition-all group-hover:shadow-md">
|
||||||
|
<LogOut size={18} className="text-red-500" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium leading-none">Logout</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuItem
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarSeparator
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import {SidebarLogo} from "@/components/wrappers/dashboard/common/sidebar/logo-sidebar";
|
import {SidebarLogo} from "@/components/wrappers/dashboard/common/sidebar/logo-sidebar";
|
||||||
import {SidebarMenuCustomMain} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar-main";
|
import {SidebarMenuCustomMain} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar-main";
|
||||||
@@ -12,6 +15,7 @@ import {OrganizationCombobox} from "@/components/wrappers/dashboard/organization
|
|||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button.server";
|
import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button.server";
|
||||||
import {UpdateNotification} from "@/features/updates/components/update-notification";
|
import {UpdateNotification} from "@/features/updates/components/update-notification";
|
||||||
|
import {Book} from "lucide-react";
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const projectName = env.PROJECT_NAME;
|
const projectName = env.PROJECT_NAME;
|
||||||
@@ -33,13 +37,28 @@ export function AppSidebar() {
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
||||||
<UpdateNotification />
|
<UpdateNotification />
|
||||||
|
|
||||||
<SidebarMenu className="mb-2">
|
<SidebarFooter>
|
||||||
<SidebarMenuItem className="p-2">
|
<SidebarSeparator />
|
||||||
<LoggedInButton/>
|
<div className="flex flex-col gap-1 p-2">
|
||||||
</SidebarMenuItem>
|
<a
|
||||||
</SidebarMenu>
|
href="https://portabase.io/docs"
|
||||||
<SideBarFooterCredit/>
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors group-data-[collapsible=icon]:justify-center"
|
||||||
|
>
|
||||||
|
<Book className="size-4 shrink-0" />
|
||||||
|
<span className="group-data-[collapsible=icon]:hidden">Documentation</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SidebarMenu className="mb-2">
|
||||||
|
<SidebarMenuItem className="p-2">
|
||||||
|
<LoggedInButton/>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
<SideBarFooterCredit/>
|
||||||
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ function ThemeSelector() {
|
|||||||
isActive ? "border-primary bg-primary/5" : "border-muted/40"
|
isActive ? "border-primary bg-primary/5" : "border-muted/40"
|
||||||
)}
|
)}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// setTheme(item.value)
|
|
||||||
await authClient.updateUser({theme: item.value});
|
await authClient.updateUser({theme: item.value});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,41 +1,58 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import { Moon, Sun, Monitor } from "lucide-react"
|
||||||
import {Moon, Sun} from "lucide-react"
|
import { authClient } from "@/lib/auth/auth-client"
|
||||||
import {Button} from "@/components/ui/button"
|
import { motion } from "motion/react"
|
||||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
import { useTheme } from "next-themes"
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import { cn } from "@/lib/utils"
|
||||||
import {toast} from "sonner";
|
|
||||||
|
const themes = [
|
||||||
|
{ id: "light", icon: Sun, label: "Light" },
|
||||||
|
{ id: "system", icon: Monitor, label: "System" },
|
||||||
|
{ id: "dark", icon: Moon, label: "Dark" },
|
||||||
|
] as const
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const setTheme = async (theme: "light" | "dark" | "system") => {
|
|
||||||
toast.success("Your theme preference has been updated.");
|
const currentIndex = themes.findIndex((t) => t.id === theme)
|
||||||
await authClient.updateUser({theme: theme});
|
const activeIndex = currentIndex === -1 ? 1 : currentIndex
|
||||||
};
|
|
||||||
|
const handleThemeChange = async (newTheme: "light" | "system" | "dark") => {
|
||||||
|
await authClient.updateUser({ theme: newTheme })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<div className="flex items-center justify-center rounded-full bg-muted/50 p-1 border border-border/50 relative w-fit">
|
||||||
<DropdownMenuTrigger asChild>
|
<motion.div
|
||||||
<Button variant="ghost" size="sm">
|
className="absolute h-7 w-8 rounded-full bg-background shadow-sm z-0 border border-border/20"
|
||||||
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"/>
|
initial={false}
|
||||||
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"/>
|
animate={{ x: activeIndex * 32 }}
|
||||||
<span className="sr-only">Toggle theme</span>
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||||
</Button>
|
style={{ left: "4px" }}
|
||||||
</DropdownMenuTrigger>
|
/>
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
<div className="flex gap-0 relative z-10">
|
||||||
Light
|
{themes.map((t) => {
|
||||||
</DropdownMenuItem>
|
const Icon = t.icon
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
const isActive = theme === t.id
|
||||||
Dark
|
|
||||||
</DropdownMenuItem>
|
return (
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
<button
|
||||||
System
|
key={t.id}
|
||||||
</DropdownMenuItem>
|
onClick={() => handleThemeChange(t.id)}
|
||||||
</DropdownMenuContent>
|
className={cn(
|
||||||
</DropdownMenu>
|
"flex h-7 w-8 items-center justify-center rounded-full transition-colors duration-200 hover:text-foreground outline-none",
|
||||||
|
isActive ? "text-primary" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
aria-label={t.label}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user