This commit is contained in:
Théo LAGACHE
2026-02-12 15:48:51 +01:00
parent 7b9cb2ecb9
commit 6f6c1c7c52
21 changed files with 133 additions and 169 deletions
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2024 Soluce Technologies Copyright 2024 Portabase
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
+1 -1
View File
@@ -16,7 +16,7 @@ export default async function GuardPage() {
return ( return (
<TooltipProvider> <TooltipProvider>
<CardAuth className="w-full max-w-md"> <CardAuth className="w-full">
<CardHeader> <CardHeader>
<div className="grid gap-2 text-center mb-2"> <div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Two-factor verification</h1> <h1 className="text-3xl font-bold">Two-factor verification</h1>
+13 -6
View File
@@ -2,6 +2,8 @@ import React from "react";
import {redirect} from "next/navigation"; import {redirect} from "next/navigation";
import {currentUser} from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section"; import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
import {env} from "@/env.mjs";
import {Heart} from "lucide-react";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({children}: { children: React.ReactNode }) {
@@ -12,13 +14,18 @@ export default async function Layout({children}: { children: React.ReactNode })
} }
return ( return (
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 "> <div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
<div className="mx-auto w-full max-w-md"> <div className="flex flex-col items-center justify-center flex-1">
<AuthLogoSection/> <div className="mx-auto w-full max-w-md">
<div>{children}</div> <AuthLogoSection/>
<div className="mt-4">{children}</div>
</div>
</div> </div>
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground"> <footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
Powered by <span className="font-medium">Soluce Technologies</span> <p className="flex items-center justify-center gap-1">
Made with <Heart className="size-3 fill-red-500 text-red-500" /> by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer> </footer>
</div> </div>
) )
+4 -3
View File
@@ -1,9 +1,10 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form"; import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { auth } from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import {CardAuth} from "@/features/layout/card-auth";
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) { export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
@@ -23,7 +24,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
return ( return (
<TooltipProvider> <TooltipProvider>
<Card className="w-full max-w-md shadow-lg"> <CardAuth className="w-full">
<CardHeader className="space-y-4"> <CardHeader className="space-y-4">
<div className="space-y-1 text-center"> <div className="space-y-1 text-center">
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1> <h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
@@ -53,7 +54,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
<CardContent> <CardContent>
<ResetPasswordForm /> <ResetPasswordForm />
</CardContent> </CardContent>
</Card> </CardAuth>
</TooltipProvider> </TooltipProvider>
); );
} }
+1 -1
View File
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-4 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className
@@ -41,9 +41,6 @@ export const AuthLogoSection = () => {
/> />
)} )}
<span className="absolute bottom-8 right-5 text-sm text-muted-foreground" style={style}>
v{env.NEXT_PUBLIC_PROJECT_VERSION}
</span>
</div> </div>
); );
}; };
@@ -68,7 +68,7 @@ export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
/> />
<div className="flex flex-col items-center gap-y-6 w-full"> <div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}> <ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Send reset link Send reset link
</ButtonWithLoading> </ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline"> <Link href="/login" className="group flex items-center text-sm hover:underline">
@@ -113,7 +113,7 @@ export const LoginForm = (props: loginFormProps) => {
</FormItem> </FormItem>
)} )}
/> />
<ButtonWithLoading className="mt-2" isPending={mutation.isPending}> <ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login Login
</ButtonWithLoading> </ButtonWithLoading>
</Form> </Form>
@@ -82,7 +82,7 @@ export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
/> />
<div className="flex flex-col items-center gap-y-6 w-full"> <div className="flex flex-col items-center gap-y-6 w-full">
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}> <ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
Reset Reset
</ButtonWithLoading> </ButtonWithLoading>
<Link href="/login" className="group flex items-center text-sm hover:underline"> <Link href="/login" className="group flex items-center text-sm hover:underline">
@@ -55,7 +55,7 @@ export const RegisterForm = (props: registerFormProps) => {
<CardHeader> <CardHeader>
<div className="grid gap-2 text-center mb-2"> <div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Create an account</h1> <h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-balance text-muted-foreground">Enter your informations bellow to register</p> <p className="text-balance text-muted-foreground">Enter your informations below to register</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -138,7 +138,7 @@ export const RegisterForm = (props: registerFormProps) => {
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" disabled={mutation.isPending}> <Button type="submit" className="h-11" disabled={mutation.isPending}>
Sign up Sign up
</Button> </Button>
<div className="mt-4 text-center text-sm"> <div className="mt-4 text-center text-sm">
@@ -47,7 +47,7 @@ export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
<CardHeader> <CardHeader>
<div className="grid gap-2 text-center mb-2"> <div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Reset Password</h1> <h1 className="text-3xl font-bold">Reset Password</h1>
<p className="text-balance text-muted-foreground">Fill information bellow to change your <p className="text-balance text-muted-foreground">Fill information below to change your
password</p> password</p>
</div> </div>
</CardHeader> </CardHeader>
@@ -14,6 +14,10 @@ interface tablePaginationProps {
export function TablePagination(props: tablePaginationProps) { export function TablePagination(props: tablePaginationProps) {
const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props; const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props;
const totalPages = table.getPageCount();
if (totalPages <= 1) return null;
return ( return (
<div <div
className={cn("flex gap-x-4", className)} className={cn("flex gap-x-4", className)}
@@ -5,9 +5,7 @@ import {Button} from "@/components/ui/button";
import {Copy, Check, Eye, EyeOff, Terminal, Key, Info} 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 {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
import { cn } from "@/lib/utils";
export type AgentCardKeyProps = { export type AgentCardKeyProps = {
edgeKey: string; edgeKey: string;
@@ -29,26 +27,14 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
return ( return (
<div className="flex flex-col gap-4 py-2"> <div className="flex flex-col gap-4 py-2">
<Tabs defaultValue="automatic" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="automatic" className="gap-2">
<Terminal className="h-4 w-4" />
<span>Automatic</span>
</TabsTrigger>
<TabsTrigger value="manual" className="gap-2">
<Key className="h-4 w-4" />
<span>Manual</span>
</TabsTrigger>
</TabsList>
<TabsContent value="automatic" className="mt-4 focus-visible:outline-none">
<Card className="border-muted/60 shadow-none py-0"> <Card className="border-muted/60 shadow-none py-0">
<CardHeader className="pb-3 px-4 pt-4"> <CardHeader className="pb-3 px-4 pt-4">
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight"> <CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
CLI Setup CLI Setup
</CardTitle> </CardTitle>
<CardDescription className="text-xs leading-relaxed"> <CardDescription className="text-xs leading-relaxed">
Click the input below to reveal the key and copy it to your terminal. To setup your agent using the CLI, copy the command below and paste it in your terminal.
Make sure you have the PortaBase CLI installed. If not, you can <a href="https://portabase.io/docs/cli" target="_blank" className="underline hover:text-primary">install it here</a>.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4 space-y-4"> <CardContent className="px-4 pb-4 space-y-4">
@@ -95,37 +81,17 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
)} )}
</Button> </Button>
</div> </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> </CardContent>
</Card> </Card>
</TabsContent>
<TabsContent value="manual" className="mt-4 focus-visible:outline-none"> <Card className="border-muted/60 shadow-none py-0">
<Card className="border-muted/60 shadow-none py-0">
<CardHeader className="pb-3 px-4 pt-4"> <CardHeader className="pb-3 px-4 pt-4">
<CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight"> <CardTitle className="text-sm font-semibold flex items-center gap-2 uppercase tracking-tight">
Registration Key Manual Setup
</CardTitle> </CardTitle>
<CardDescription className="text-xs leading-relaxed"> <CardDescription className="text-xs leading-relaxed">
Use this key to manually configure your agent in your configuration file. Use this key to manually configure your agent in your configuration file.
If you need help for manual configuration, you can follow our guide in the <a href="https://portabase.io/docs" target="_blank" className="underline hover:text-primary">documentation</a>.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4 space-y-4"> <CardContent className="px-4 pb-4 space-y-4">
@@ -172,44 +138,8 @@ export const AgentCardKey = ({edgeKey, agentName}: AgentCardKeyProps) => {
)} )}
</Button> </Button>
</div> </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> </CardContent>
</Card> </Card>
</TabsContent>
</Tabs>
</div> </div>
); );
}; };
@@ -90,7 +90,8 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
</Accordion> </Accordion>
</div> </div>
<div className="space-y-6"> {agent.databases.length > 0 && (
<div className="space-y-6">
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Managed Databases</h2> <h2 className="text-2xl font-bold tracking-tight">Managed Databases</h2>
@@ -107,6 +108,7 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
cardItem={AgentDatabaseCard} cardItem={AgentDatabaseCard}
/> />
</div> </div>
)}
</div> </div>
) )
} }
@@ -47,20 +47,6 @@ export const SidebarMenuCustomMain = () => {
type: "list", type: "list",
group_content: groupContent, group_content: groupContent,
}, },
{
label: "Resources",
type: "list",
group_content: [
{
title: "Documentation",
url: "https://portabase.io/docs",
icon: BookOpen,
type: "item",
redirect: true,
not_from_base_url: true,
}
],
},
]; ];
@@ -118,6 +104,23 @@ export const SidebarMenuCustomMain = () => {
}); });
} }
items.push(
{
label: "Resources",
type: "list",
group_content: [
{
title: "Documentation",
url: "https://portabase.io/docs",
icon: BookOpen,
type: "item",
redirect: true,
not_from_base_url: true,
}
],
},
)
return <SidebarMenuCustomBase baseUrl={BASE_URL} items={items}/>; return <SidebarMenuCustomBase baseUrl={BASE_URL} items={items}/>;
}; };
@@ -16,8 +16,6 @@ import {useMutation, useQueryClient} from "@tanstack/react-query";
import {BackupStorageWith} from "@/db/schema/14_storage-backup"; import {BackupStorageWith} from "@/db/schema/14_storage-backup";
import {TooltipProvider} from "@/components/ui/tooltip"; import {TooltipProvider} from "@/components/ui/tooltip";
import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common"; import {getChannelIcon} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
import {truncateWords} from "@/utils/text";
import {useIsMobile} from "@/hooks/use-mobile";
import {Badge} from "@/components/ui/badge"; import {Badge} from "@/components/ui/badge";
import {getStatusColor, getStatusIcon} from "@/components/wrappers/dashboard/admin/notifications/logs/columns"; import {getStatusColor, getStatusIcon} from "@/components/wrappers/dashboard/admin/notifications/logs/columns";
import { import {
@@ -39,7 +37,6 @@ type BackupActionsFormProps = {
export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => { export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? [] const filteredBackupStorages = backup.storages?.filter((storage) => storage.deletedAt === null) ?? []
const isMobile = useIsMobile();
const {closeModal} = useBackupModal(); const {closeModal} = useBackupModal();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -175,24 +172,26 @@ export const BackupActionsForm = ({backup, action}: BackupActionsFormProps) => {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0 flex flex-col gap-1"> <div className="flex-1 min-w-0 flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 min-w-0">
{getChannelIcon(storage.storageChannel?.provider || "")} <div className="shrink-0">
<h3 className="font-medium text-foreground"> {getChannelIcon(storage.storageChannel?.provider || "")}
{isMobile ? truncateWords(storage?.storageChannel?.name ?? "", 2) : storage.storageChannel?.name} </div>
</h3> <h3 className="font-medium text-foreground truncate">
<Badge variant="secondary" {storage.storageChannel?.name}
className="text-xs font-mono"> </h3>
{storage.storageChannel?.provider} <Badge variant="secondary"
className="text-xs font-mono shrink-0">
{storage.storageChannel?.provider}
</Badge>
</div>
<Badge variant="outline"
className={`gap-1.5 shrink-0 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge> </Badge>
</div> </div>
<Badge variant="outline"
className={`gap-1.5 ${getStatusColor(storage.status)}`}>
{getStatusIcon(storage.status === "success")}
<span
className="capitalize">{storage.status.toUpperCase()}</span>
</Badge>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -113,7 +113,7 @@ export function OrganizationCombobox() {
: "focus:bg-accent hover:bg-accent/50 border border-transparent" : "focus:bg-accent hover:bg-accent/50 border border-transparent"
)}> )}>
<div className={cn( <div className={cn(
"flex size-9 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md", "flex size-9 shrink-0 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
org.logo ? "bg-transparent border-transparent" : "", org.logo ? "bg-transparent border-transparent" : "",
isActive && !org.logo ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border" isActive && !org.logo ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border"
)}> )}>
@@ -126,7 +126,7 @@ export function OrganizationCombobox() {
)}/> )}/>
)} )}
</div> </div>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5 flex-1 min-w-0">
<span className={cn( <span className={cn(
"text-sm max-w-42.5 truncate font-medium leading-none", "text-sm max-w-42.5 truncate font-medium leading-none",
isActive ? "text-primary" : "" isActive ? "text-primary" : ""
@@ -145,7 +145,7 @@ export default function TwoFactorForm({ onSuccess, onSuccessData }: TwoFactorFor
/> />
<div className="flex flex-col gap-3 pt-2"> <div className="flex flex-col gap-3 pt-2">
<Button type="submit" disabled={isPending}> <Button type="submit" className="h-11" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Verify Verify
</Button> </Button>
@@ -21,7 +21,7 @@ export function ProfileAppearance() {
} }
function ThemeSelector() { function ThemeSelector() {
const {theme, setTheme} = useTheme(); const {theme} = useTheme();
return ( return (
+48 -29
View File
@@ -1,58 +1,77 @@
"use client" "use client"
import { Moon, Sun, Check, SunMoon } from "lucide-react"
import { Moon, Sun, Monitor } from "lucide-react"
import { authClient } from "@/lib/auth/auth-client"
import { motion } from "motion/react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { authClient } from "@/lib/auth/auth-client"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const themes = [ const themes = [
{ id: "light", icon: Sun, label: "Light" }, { id: "light", icon: Sun, label: "Light" },
{ id: "system", icon: Monitor, label: "System" },
{ id: "dark", icon: Moon, label: "Dark" }, { id: "dark", icon: Moon, label: "Dark" },
{ id: "system", icon: SunMoon, label: "System" },
] as const ] as const
export function ModeToggle() { export function ModeToggle() {
const { theme } = useTheme() const { theme } = useTheme()
const currentIndex = themes.findIndex((t) => t.id === theme)
const activeIndex = currentIndex === -1 ? 1 : currentIndex
const handleThemeChange = async (newTheme: "light" | "system" | "dark") => { const handleThemeChange = async (newTheme: "light" | "system" | "dark") => {
await authClient.updateUser({ theme: newTheme }) await authClient.updateUser({ theme: newTheme })
} }
return ( return (
<div className="flex items-center justify-center rounded-full bg-muted/50 p-0.5 border border-border/50 relative w-fit"> <DropdownMenu>
<motion.div <DropdownMenuTrigger asChild>
className="absolute h-6 w-7 rounded-full bg-background shadow-sm z-0 border border-border/20" <Button variant="ghost" size="icon" className="size-8 rounded-full border border-input bg-transparent shadow-xs transition-transform active:scale-95">
initial={false} {theme === "light" ? <Sun className="size-4" /> : theme === "dark" ? <Moon className="size-4" /> : <SunMoon className="size-4" />}
animate={{ x: activeIndex * 28 }} <span className="sr-only">Toggle theme</span>
transition={{ type: "spring", stiffness: 400, damping: 30 }} </Button>
style={{ left: "2px" }} </DropdownMenuTrigger>
/> <DropdownMenuContent align="end" className="min-w-37.5 rounded-xl border-2 border-border p-1 shadow-none">
<div className="flex gap-0 relative z-10">
{themes.map((t) => { {themes.map((t) => {
const Icon = t.icon const ThemeIcon = t.icon
const isActive = theme === t.id const isActive = theme === t.id
return ( return (
<button <DropdownMenuItem
key={t.id} key={t.id}
onClick={() => handleThemeChange(t.id)} onClick={() => handleThemeChange(t.id)}
className={cn( className={cn(
"flex h-6 w-7 items-center justify-center rounded-full transition-colors duration-200 hover:text-foreground outline-none", "group gap-2 p-2 cursor-pointer rounded-lg mb-1 last:mb-0 transition-colors",
isActive ? "text-primary" : "text-muted-foreground" isActive
? "bg-primary/10 text-primary border border-primary/20"
: "focus:bg-accent hover:bg-accent/50 border border-transparent"
)} )}
aria-label={t.label}
> >
<Icon className="h-3.5 w-3.5" /> <div className={cn(
</button> "flex size-7 shrink-0 items-center justify-center rounded-md border shadow-sm transition-all group-hover:shadow-md",
isActive ? "bg-primary text-primary-foreground border-primary/30" : "bg-muted/50 border-border"
)}>
<ThemeIcon className={cn(
"size-4",
isActive ? "text-primary-foreground" : "text-muted-foreground"
)} />
</div>
<span className={cn(
"text-sm font-medium leading-none flex-1",
isActive ? "text-primary" : ""
)}>
{t.label}
</span>
{isActive && (
<div className="flex size-4 items-center justify-center rounded-full bg-primary shadow-sm ml-auto">
<Check className="size-2.5 text-primary-foreground" strokeWidth={3} />
</div>
)}
</DropdownMenuItem>
) )
})} })}
</div> </DropdownMenuContent>
</div> </DropdownMenu>
) )
} }
@@ -18,9 +18,9 @@ export const UpdateNotification = () => {
<SidebarGroup className="py-0"> <SidebarGroup className="py-0">
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem className="px-2"> <SidebarMenuItem>
<div <div
className="relative flex flex-col gap-2 rounded-lg border bg-primary/5 p-3 text-sidebar-foreground border-primary/20"> className="w-[var(--radix-popper-anchor-width)] relative flex flex-col gap-2 rounded-lg border bg-primary/5 p-3 text-sidebar-foreground border-primary/20">
<button <button
onClick={dismissUpdate} onClick={dismissUpdate}
className="absolute right-2 top-2 rounded-md p-0.5 text-muted-foreground/50 hover:bg-sidebar-accent hover:text-foreground transition-colors" className="absolute right-2 top-2 rounded-md p-0.5 text-muted-foreground/50 hover:bg-sidebar-accent hover:text-foreground transition-colors"
@@ -37,10 +37,12 @@ export const UpdateNotification = () => {
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-[10px] font-semibold leading-none">Update available</span> <span className="text-[10px] font-semibold leading-none">Update available</span>
<span {newRelease.tag_name && (
className="text-[10px] text-muted-foreground font-medium px-1 py-0.5 bg-primary/10 rounded-full"> <span
v{newRelease.tag_name.replace(/^v/, "")} className="text-[10px] text-muted-foreground font-medium px-1 py-0.5 bg-primary/10 rounded-full">
</span> v{newRelease.tag_name.replace(/^v/, "")}
</span>
)}
</div> </div>
<Link <Link
href={newRelease.html_url} href={newRelease.html_url}