add theme switcher and improve org switcher

This commit is contained in:
Milo Schwartz 2024-12-22 20:16:52 -05:00
parent af2d78cbfb
commit b1afba191e
No known key found for this signature in database
4 changed files with 106 additions and 49 deletions

View file

@ -448,11 +448,11 @@ authRouter.post(
verifySessionMiddleware, verifySessionMiddleware,
auth.requestEmailVerificationCode auth.requestEmailVerificationCode
); );
authRouter.post( // authRouter.post(
"/change-password", // "/change-password",
verifySessionUserMiddleware, // verifySessionUserMiddleware,
auth.changePassword // auth.changePassword
); // );
authRouter.post("/reset-password/request", auth.requestPasswordReset); authRouter.post("/reset-password/request", auth.requestPasswordReset);
authRouter.post("/reset-password/", auth.resetPassword); authRouter.post("/reset-password/", auth.resetPassword);

View file

@ -10,6 +10,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator
} from "@app/components/ui/command"; } from "@app/components/ui/command";
import { import {
DropdownMenu, DropdownMenu,
@ -18,12 +19,12 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger
} from "@app/components/ui/popover"; } from "@app/components/ui/popover";
import { import {
Select, Select,
@ -31,13 +32,23 @@ import {
SelectGroup, SelectGroup,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { cn, formatAxiosError } from "@app/lib/utils"; import { cn, formatAxiosError } from "@app/lib/utils";
import { ListOrgsResponse } from "@server/routers/org"; import { ListOrgsResponse } from "@server/routers/org";
import { Check, ChevronsUpDown, Plus } from "lucide-react"; import {
Check,
ChevronsUpDown,
Laptop,
LogOut,
Moon,
Plus,
Sun,
User
} from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@ -51,8 +62,12 @@ type HeaderProps = {
export default function Header({ email, orgId, name, orgs }: HeaderProps) { export default function Header({ email, orgId, name, orgs }: HeaderProps) {
const { toast } = useToast(); const { toast } = useToast();
const { setTheme, theme } = useTheme();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">(
theme as "light" | "dark" | "system"
);
const router = useRouter(); const router = useRouter();
@ -72,7 +87,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
console.error("Error logging out", e); console.error("Error logging out", e);
toast({ toast({
title: "Error logging out", title: "Error logging out",
description: formatAxiosError(e, "Error logging out"), description: formatAxiosError(e, "Error logging out")
}); });
}) })
.then(() => { .then(() => {
@ -80,6 +95,11 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
}); });
} }
function handleThemeChange(theme: "light" | "dark" | "system") {
setUserTheme(theme);
setTheme(theme);
}
return ( return (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -104,22 +124,54 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
> >
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
{name && ( <p className="text-sm font-medium leading-none">
<p className="text-sm font-medium leading-none truncate"> Signed in as
{name}
</p> </p>
)} <p className="text-xs leading-none text-muted-foreground">
<p className="text-xs leading-none text-muted-foreground truncate">
{email} {email}
</p> </p>
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuItem>
<DropdownMenuItem onClick={logout}> <User className="mr-2 h-4 w-4" />
Logout <span>User Settings</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Theme</DropdownMenuLabel>
{(["light", "dark", "system"] as const).map(
(themeOption) => (
<DropdownMenuItem
key={themeOption}
onClick={() =>
handleThemeChange(themeOption)
}
>
{themeOption === "light" && (
<Sun className="mr-2 h-4 w-4" />
)}
{themeOption === "dark" && (
<Moon className="mr-2 h-4 w-4" />
)}
{themeOption === "system" && (
<Laptop className="mr-2 h-4 w-4" />
)}
<span className="capitalize">
{themeOption}
</span>
{userTheme === themeOption && (
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="h-2 w-2 rounded-full bg-primary"></span>
</span>
)}
</DropdownMenuItem>
)
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<span className="truncate max-w-[150px] md:max-w-none font-medium"> <span className="truncate max-w-[150px] md:max-w-none font-medium">
@ -163,7 +215,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
{orgId {orgId
? orgs.find( ? orgs.find(
(org) => (org) =>
org.orgId === orgId, org.orgId === orgId
)?.name )?.name
: "Select organization..."} : "Select organization..."}
</span> </span>
@ -176,25 +228,30 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
<Command> <Command>
<CommandInput placeholder="Search..." /> <CommandInput placeholder="Search..." />
<CommandEmpty> <CommandEmpty>
No organization found. No organizations found.
</CommandEmpty> </CommandEmpty>
<CommandGroup className="[50px]"> <CommandGroup heading="Create">
<CommandList> <CommandList>
<CommandItem <CommandItem
className="flex items-center border border-input mb-2 cursor-pointer" className="flex items-center cursor-pointer"
onSelect={(currentValue) => { onSelect={(currentValue) => {
router.push("/setup"); router.push("/setup");
}} }}
> >
<Plus className="mr-2 h-4 w-4"/> <Plus className="mr-2 h-4 w-4" />
New Organization New Organization
</CommandItem> </CommandItem>
</CommandList>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Organizations">
<CommandList>
{orgs.map((org) => ( {orgs.map((org) => (
<CommandItem <CommandItem
key={org.orgId} key={org.orgId}
onSelect={(currentValue) => { onSelect={(currentValue) => {
router.push( router.push(
`/${org.orgId}/settings`, `/${org.orgId}/settings`
); );
}} }}
> >
@ -203,7 +260,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) {
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
orgId === org.orgId orgId === org.orgId
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0"
)} )}
/> />
{org.name} {org.name}

View file

@ -110,27 +110,6 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
</div> </div>
<div className="container mx-auto sm:px-0 px-3 pt-[165px]">{children}</div> <div className="container mx-auto sm:px-0 px-3 pt-[165px]">{children}</div>
<footer className="w-full mt-6 py-3">
<div className="container mx-auto flex justify-end items-center px-3 sm:px-0 text-sm text-neutral-300 dark:text-neutral-700 space-x-3 select-none">
<div>Built by Fossorial</div>
<a
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-4 h-4"
>
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
</svg>
</a>
</div>
</footer>
</> </>
); );
} }

View file

@ -38,6 +38,27 @@ export default async function RootLayout({
}} }}
> >
{children} {children}
<footer className="w-full mt-6 py-3">
<div className="container mx-auto flex justify-center items-center px-3 sm:px-0 text-sm text-neutral-300 dark:text-neutral-700 space-x-3 select-none">
<div>Built by Fossorial</div>
<a
href="https://github.com/fosrl/pangolin"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-4 h-4"
>
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
</svg>
</a>
</div>
</footer>
</EnvProvider> </EnvProvider>
<Toaster /> <Toaster />
</ThemeProvider> </ThemeProvider>