Skip to content

Commit

Permalink
feat: shadcn sidebar (#752)
Browse files Browse the repository at this point in the history
* feat: add sidebar components

* feat: rework sidenav to use new sidebar component

* chore: format

* chore: make the configured collapsible type actually do something

* fix: set default value for sidebar as open instead of closed

* fix: make the chevron more easily clickable and rotate better

* fix: apply suggested fix for incorrect cookie behavior

* fix: make sidebar viewable

* fix: run prettier

* chore: restore layout

* fix: make more like designs

* fix: make thing good

* fix: perfect

* fix: fix sidebar colors

* fix: fix cookie awaiting

* fix: fix padding of avatar

* fix: ignore eslint warning

* fix: tweak sidebar colors ever so slightly

* feat: expand sidebar on clicking submenu in icon mode

* fix: fix type errors

* fix: prettier pass

* fix: fix icon

* fix: make login switcher more accessible and easy to reach by tests

* fix: make login test use different emails making them easier to run in isolation

* fix: fix some hydration/control errors so login tests can run in dev mode (otherwise error message obscures loginSwitcher)

* feat: change all slate to gray

* feat: distinguish between accent (hover) and active states

* feat: change all slate to gray in core

* fix: fix accessibility error

* feat: add specific auth checks for sidebar

* fix: eslint

* fix: increase timeout for pub capabilities test

* fix: change odd useeffect

* fix: fix spacing and shadow issues

* fix: make last element of sidebar group clickable in collapsed mode

* feat: add tooltips in collapsed mode
  • Loading branch information
tefkah authored Jan 23, 2025
1 parent 730195c commit 5eebab0
Show file tree
Hide file tree
Showing 63 changed files with 1,831 additions and 481 deletions.
2 changes: 1 addition & 1 deletion config/prettier/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const config = {
// "prettier-plugin-jsdoc",
],
// tailwindConfig: fileURLToPath(
// new URL("../../tooling/tailwind/web.ts", import.meta.url),
// new URL("../../packages/ui/tailwind.config.cjs", import.meta.url)
// ),
tailwindFunctions: ["cn", "cva"],
importOrder: [
Expand Down
4 changes: 4 additions & 0 deletions core/app/(user)/forgot/ForgotForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const forgotPasswordSchema = z.object({
export default function ForgotForm() {
const form = useForm<z.infer<typeof forgotPasswordSchema>>({
resolver: zodResolver(forgotPasswordSchema),
defaultValues: {
// in order to prevent "Form changed from uncontrolled to controlled" React errors
email: "",
},
});

const sendForgotPasswordMail = useServerAction(actions.sendForgotPasswordMail);
Expand Down
7 changes: 6 additions & 1 deletion core/app/(user)/login/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export default function LoginForm() {
const searchParams = useSearchParams();
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
// in order to prevent "Form changed from uncontrolled to controlled" React errors
email: "",
password: "",
},
});

const runLoginWithPassword = useServerAction(actions.loginWithPassword);
Expand Down Expand Up @@ -55,7 +60,7 @@ export default function LoginForm() {
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="m@example.com" {...field} />
<Input placeholder="name@example.com" {...field} />
</FormControl>
</FormItem>
</div>
Expand Down
12 changes: 7 additions & 5 deletions core/app/(user)/reset/ResetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default function ResetForm() {
const router = useRouter();
const form = useForm<z.infer<typeof resetPasswordSchema>>({
resolver: zodResolver(resetPasswordSchema),
defaultValues: {
// in order to prevent "Form changed from uncontrolled to controlled" React errors
password: "",
},
});
const runResetPassword = useServerAction(resetPassword);

Expand Down Expand Up @@ -84,11 +88,9 @@ export default function ResetForm() {
<DialogContent>
<DialogHeader>
<DialogTitle>Success</DialogTitle>
<DialogDescription>
<p className="flex flex-col gap-2">
<span className="text-green-700">Success - password reset!</span>
Redirecting in 5 seconds...
</p>
<DialogDescription className="flex flex-col gap-2">
<span className="text-green-700">Success - password reset!</span>
Redirecting in 5 seconds...
</DialogDescription>
</DialogHeader>
</DialogContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const RequestLink = ({
return (
<Button
variant="secondary"
className="bg-blue-500 text-slate-50 shadow-sm hover:bg-blue-500/90 dark:bg-blue-900 dark:text-slate-50 dark:hover:bg-blue-900/90"
className="bg-blue-500 text-gray-50 shadow-sm hover:bg-blue-500/90 dark:bg-blue-900 dark:text-gray-50 dark:hover:bg-blue-900/90"
onClick={requestLink}
>
<Mail size={16} className="mr-1" strokeWidth={1} /> Request New Link
Expand Down
26 changes: 17 additions & 9 deletions core/app/c/[communitySlug]/CommunitySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "ui/dropdown-menu";
import { ChevronDown, ChevronsUpDown } from "ui/icon";
import { SidebarMenuButton } from "ui/sidebar";
import { cn } from "utils";

import type { CommunityData } from "~/lib/server/community";

Expand All @@ -16,21 +19,24 @@ type Props = {
};

const CommunitySwitcher: React.FC<Props> = function ({ community, availableCommunities }) {
const avatarClasses = "rounded w-9 h-9 mr-2";
const textClasses = "flex-auto text-base font-bold w-44 text-left";
const avatarClasses =
"rounded-md w-9 h-9 mr-1 group-data-[collapsible=icon]:h-8 group-data-[collapsible=icon]:w-8 border";
const textClasses = "flex-auto text-base font-semibold w-44 text-left";
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex cursor-pointer items-center rounded p-1 hover:bg-gray-200 md:p-2">
{/* <div className="flex cursor-pointer items-center rounded p-1 hover:bg-gray-200 md:p-2"> */}
<SidebarMenuButton className={`h-full group-data-[collapsible=icon]:!p-0 md:py-1`}>
<Avatar className={avatarClasses}>
<AvatarImage src={community.avatar || undefined} />
<AvatarFallback>{community.name[0]}</AvatarFallback>
</Avatar>
<div className={textClasses}>{community.name}</div>
<img className="" src="/icons/chevron-vertical.svg" alt="" />
</div>
<span className={textClasses}>{community.name}</span>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
{/* </div> */}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 bg-white">
<DropdownMenuContent className="w-[--radix-popper-anchor-width] min-w-52" side="right">
{availableCommunities
.filter((option) => {
return option?.slug !== community.slug;
Expand All @@ -42,12 +48,14 @@ const CommunitySwitcher: React.FC<Props> = function ({ community, availableCommu
href={`/c/${option.slug}/stages`}
className="cursor-pointer hover:bg-gray-50"
>
<div className="flex items-center">
<div className="flex items-center gap-2">
<Avatar className={avatarClasses}>
<AvatarImage src={option.avatar || undefined} />
<AvatarFallback>{option.name[0]}</AvatarFallback>
</Avatar>
<div className={textClasses}>{option.name}</div>
<span className={cn(textClasses, "font-medium")}>
{option.name}
</span>
</div>
</Link>
</DropdownMenuItem>
Expand Down
7 changes: 6 additions & 1 deletion core/app/c/[communitySlug]/ContentLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { ReactNode } from "react";

import { SidebarTrigger } from "ui/sidebar";

import { COLLAPSIBLE_TYPE } from "./SideNav";

const Heading = ({
title,
left,
Expand All @@ -10,7 +14,8 @@ const Heading = ({
right?: ReactNode;
}) => {
return (
<header className="z-40 flex h-[72px] items-center justify-between border-b bg-gray-50 p-4 shadow-md">
<header className="z-20 flex h-[72px] items-center justify-between border-b bg-gray-50 p-4 shadow-md">
{COLLAPSIBLE_TYPE === "icon" ? null : <SidebarTrigger />}
{left}
<h1 className="text-lg font-semibold">
<div className="flex flex-row items-center">{title}</div>
Expand Down
89 changes: 64 additions & 25 deletions core/app/c/[communitySlug]/LoginSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,81 @@
import type { User } from "lucia";

import Link from "next/link";

import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar";
import { Button } from "ui/button";
import { Settings } from "ui/icon";
import { ChevronsUpDown, UserRoundCog } from "ui/icon";
import { Popover, PopoverContent, PopoverTrigger } from "ui/popover";
import { Separator } from "ui/separator";
import { SidebarMenuButton } from "ui/sidebar";

import { getLoginData } from "~/lib/authentication/loginData";
import LogoutButton from "../../components/LogoutButton";

const AvatarThing = ({ user }: { user: User }) => (
<div className="flex w-full items-center gap-x-2">
<Avatar className="h-9 w-9 group-data-[collapsible=icon]:-ml-2 group-data-[collapsible=icon]:h-8 group-data-[collapsible=icon]:w-8">
<AvatarImage src={user.avatar || undefined} />
<AvatarFallback>{(user.firstName || user.email)[0].toUpperCase()}</AvatarFallback>
</Avatar>

<div className="flex min-w-0 flex-grow flex-col justify-start text-start group-data-[collapsible=icon]:hidden">
<p className="truncate text-sm">{user.firstName}</p>
<p className="truncate text-xs text-gray-500">{user.email}</p>
</div>
</div>
);

export default async function LoginSwitcher() {
const { user } = await getLoginData();
if (!user) {
return null;
}
return (
<div className="w-max-[100%] flex flex-col gap-y-2 rounded-lg border border-gray-100 bg-white p-2">
<div className="flex items-center">
<Avatar className="mr-2 h-9 w-9">
<Link className="w-full" href="/settings">
<AvatarImage src={user.avatar || undefined} />
<AvatarFallback>
{(user.firstName || user.email)[0].toUpperCase()}
</AvatarFallback>
</Link>
</Avatar>
<div>
<div className="text-xs">{user.firstName}</div>
<div className="text-xs text-gray-400">{user.email}</div>
</div>
</div>
<div className="mt-1 flex flex-row items-center">
<LogoutButton />
<Button variant="outline" size="sm" asChild>
<Link className="ml-2 flex items-center gap-1" href="/settings">
<Settings size="14" />
Settings
</Link>
</Button>
</div>
<div className="w-max-[100%] borderp-2 flex flex-col gap-y-2 rounded-lg">
<Popover>
<PopoverTrigger asChild>
<SidebarMenuButton
className="flex h-fit items-center gap-x-2 p-2 py-1"
aria-label="User menu"
data-testid="user-menu-button"
aria-haspopup="true"
>
<AvatarThing user={user} />
<ChevronsUpDown
size="16"
className="group-data-[collapsible=icon]:hidden"
/>
</SidebarMenuButton>
</PopoverTrigger>
<PopoverContent side="right" className="p-0">
<div className="flex flex-col items-start">
<div className="p-2">
<AvatarThing user={user} />
</div>
<Separator className="mx-1" />
<Button
variant="ghost"
size="sm"
asChild
className="w-full justify-start gap-2 rounded-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<Link
className="flex w-full items-center justify-start gap-2"
href="/settings"
>
<UserRoundCog size="14" strokeWidth={1.5} />
Settings
</Link>
</Button>
<Separator className="mx-1" />
<LogoutButton
variant="ghost"
className="w-full justify-start rounded-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}
29 changes: 0 additions & 29 deletions core/app/c/[communitySlug]/NavLink.module.css

This file was deleted.

79 changes: 61 additions & 18 deletions core/app/c/[communitySlug]/NavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,73 @@

import * as React from "react";
import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";
import { usePathname } from "next/navigation";

import { SidebarMenuButton, SidebarMenuSubButton, useSidebar } from "ui/sidebar";
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
import { cn } from "utils";

type Props = { href: string; text: string; icon: React.ReactNode; count?: number };
type Props = {
href: string;
text: string;
icon?: React.ReactNode;
count?: number;
isChild?: boolean;
// optional pattern to match the pathname against for showing the active state
// by default it's `c\\/.*?${href}`
pattern?: string;
hasChildren?: boolean;
groupName?: string;
};

export default function NavLink({ href, text, icon, count }: Props) {
const layoutSegment = useSelectedLayoutSegment();
const isActive = layoutSegment ? new RegExp(`c\\/.*?\\/${layoutSegment}$`).test(href) : false;
return (
<Link
className={cn(
"-mx-1 flex items-center rounded-md px-1 py-3 hover:bg-gray-100",
isActive && "text-bold bg-gray-200 hover:bg-gray-200"
)}
href={href}
>
<div className="ml-2 mr-3 w-4">{icon}</div>
<div className="flex-auto text-sm font-bold">{text}</div>
export default function NavLink({
href,
groupName,
text,
icon,
count,
isChild,
hasChildren,
pattern,
}: Props) {
const pathname = usePathname();
const { state: sideBarState } = useSidebar();
const regex = React.useMemo(
() => (pattern ? new RegExp(`c\\/.*?${pattern}`) : new RegExp(href)),
[pattern, href]
);

const isActive = regex.test(pathname);

const content = (
<Link href={href} className="relative">
{icon ? icon : null}
<span className="flex-auto text-sm transition-opacity group-data-[collapsible=icon]:opacity-0">
{text}
</span>
{count && (
<div className="rounded-md border border-gray-200 px-2 text-sm font-bold">
{count}
</div>
<span className="rounded-md border border-gray-200 px-2 text-sm">{count}</span>
)}
</Link>
);

return (
<Tooltip delayDuration={300} open={sideBarState === "expanded" ? false : undefined}>
<TooltipTrigger asChild>
{isChild ? (
<SidebarMenuSubButton isActive={isActive} asChild>
{content}
</SidebarMenuSubButton>
) : (
<SidebarMenuButton isActive={isActive} asChild>
{content}
</SidebarMenuButton>
)}
</TooltipTrigger>

<TooltipContent side="right">
<p className="text-xs">{groupName ? `${groupName} - ${text}` : text}</p>
</TooltipContent>
</Tooltip>
);
}
Loading

0 comments on commit 5eebab0

Please sign in to comment.