Skip to content

Commit

Permalink
chore: cleaned up duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
cecilia-sanare committed Jan 20, 2024
1 parent 6675cba commit 3c88b3e
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 101 deletions.
Binary file modified bun.lockb
Binary file not shown.
55 changes: 55 additions & 0 deletions components/AddDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FeatureFlag } from '@/backend/services/environment.service';
import { BadgePlus, NotebookPen, Sword } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { FaDiceD20 } from 'react-icons/fa6';
import { NavigationList } from './NavigationList';
import { Button } from './ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';

export type AddDropdownProps = {
className?: string;
};

const items: NavigationList.Action<typeof DropdownMenuItem>[] = [
{
label: 'Campaign',
flag: FeatureFlag.Campaigns,
icon: FaDiceD20,
onClick: () => console.log('Campaign'),
},
{
label: 'Game',
flag: FeatureFlag.Games,
icon: NotebookPen,
onClick: () => console.log('Game'),
},
{
label: 'Character',
flag: FeatureFlag.Characters,
icon: Sword,
onClick: () => console.log('Character'),
},
];

export function AddDropdown({ className }: AddDropdownProps) {
const { data: session } = useSession();

if (!session || !session.user) {
return null;
}

return (
<DropdownMenu>
<DropdownMenuTrigger className={className} asChild>
<Button variant="secondary" size="icon">
<BadgePlus className="size-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="end">
<div className="flex flex-col gap-1">
<NavigationList items={items} as={DropdownMenuItem} flip />
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
75 changes: 31 additions & 44 deletions components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use client';
import { EnvironmentService, FeatureFlag } from '@/backend/services/environment.service';
import { FeatureFlag } from '@/backend/services/environment.service';
import { cn } from '@/lib/utils';
import { classNames } from '@rain-cafe/react-utils';
import { BadgePlus, Dices, LucideIcon, Menu, NotebookPen, Swords, X } from 'lucide-react';
import { LucideIcon, Menu, NotebookPen, Swords, X } from 'lucide-react';
import { Alice } from 'next/font/google';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { IconType } from 'react-icons';
import { FaDiceD20 } from 'react-icons/fa6';
import { AddDropdown } from './AddDropdown';
import { NavigationList } from './NavigationList';
import { Profile } from './Profile';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
Expand All @@ -17,28 +21,26 @@ const font = Alice({
fallback: ['serif'],
});

const links: Header.Link[] = (
[
{
label: 'Campaigns',
href: '/campaigns',
icon: Dices,
flag: FeatureFlag.Campaigns,
},
{
label: 'Games',
href: '/games',
icon: NotebookPen,
flag: FeatureFlag.Games,
},
{
label: 'Characters',
href: '/characters',
icon: Swords,
flag: FeatureFlag.Characters,
},
] satisfies Header.Link[]
).filter((link) => typeof link.flag === 'undefined' || EnvironmentService.enabled(link.flag));
const items: NavigationList.Action<typeof Button>[] = [
{
label: 'Campaigns',
href: '/campaigns',
icon: FaDiceD20,
flag: FeatureFlag.Campaigns,
},
{
label: 'Games',
href: '/games',
icon: NotebookPen,
flag: FeatureFlag.Games,
},
{
label: 'Characters',
href: '/characters',
icon: Swords,
flag: FeatureFlag.Characters,
},
];

export function Header() {
const pathname = usePathname();
Expand Down Expand Up @@ -75,18 +77,10 @@ export function Header() {
Charcoal
</Link>
<div className="hidden md:flex gap-4 flex-1">
{links.map((link, index) => (
<Button key={index} variant="secondary" asChild>
<Link href={link.href}>
{link.icon && <link.icon className="mr-2 size-5" />}
{link.label}
</Link>
</Button>
))}
<NavigationList items={items} as={Button} variant="ghost" />
</div>
<Button variant="secondary" className="ml-auto" size="icon">
<BadgePlus className="size-6" />
</Button>
<span className="flex-1" />
<AddDropdown />
<div className="hidden md:flex">
<Profile />
</div>
Expand Down Expand Up @@ -119,14 +113,7 @@ export function Header() {
<div className="mt-6 flex flex-col gap-4">
<Profile mobile />
<Separator decorative />
{links.map((link, index) => (
<Button key={index} variant="ghost" className="gap-4 justify-between" asChild>
<Link href={link.href}>
{link.label}
{link.icon && <link.icon />}
</Link>
</Button>
))}
<NavigationList items={items} as={Button} variant="ghost" flip />
</div>
</div>
</div>
Expand All @@ -138,7 +125,7 @@ export namespace Header {
export type Link = {
label: string;
href: string;
icon?: LucideIcon;
icon?: LucideIcon | IconType;
flag?: FeatureFlag;
};
}
85 changes: 85 additions & 0 deletions components/NavigationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { EnvironmentService, FeatureFlag } from '@/backend/services/environment.service';
import { cn } from '@/lib/utils';
import { LucideIcon } from 'lucide-react';
import Link from 'next/link';
import { ComponentProps, useMemo } from 'react';
import { IconType } from 'react-icons';
import { Button } from './ui/button';
import { DropdownMenuItem } from './ui/dropdown-menu';

export type NavigationListProps<T extends NavigationList.SupportedParent> = {
items: NavigationList.Action<T>[];
as: T;
flip?: boolean;
variant?: ComponentProps<T>['variant'];
};

export function NavigationList<T extends NavigationList.SupportedParent>({
items,
as,
flip,
variant,
}: NavigationListProps<T>) {
const filteredItems = useMemo(() => {
return items.filter((item) => typeof item.flag === 'undefined' || EnvironmentService.enabled(item.flag));
}, [items]);
const Component = as;

return (
<>
{filteredItems.map((item, index) => {
const baseProps: Pick<ComponentProps<T>, 'variant' | 'className'> = {
variant: item.variant ?? variant,
className: cn('text-sm flex justify-between gap-2', flip && 'flex-row-reverse'),
};

const content = (
<>
{item.icon && <item.icon className="size-6" />}
<span>{item.label}</span>
</>
);

if (item.href) {
return (
// TODO: Figure out how to get this to stop freaking out
// @ts-ignore
<Component {...baseProps} key={index} asChild>
<Link href={item.href}>{content}</Link>
</Component>
);
}

return (
// TODO: Figure out how to get this to stop freaking out
// @ts-ignore
<Component {...baseProps} key={index} onClick={item.onClick}>
{content}
</Component>
);
})}
</>
);
}

export namespace NavigationList {
export type SupportedParent = typeof Button | typeof DropdownMenuItem;

type BaseAction<T extends SupportedParent> = {
label: string;
icon?: LucideIcon | IconType;
flag?: FeatureFlag;
} & Pick<ComponentProps<T>, 'variant'>;

interface LinkAction<T extends SupportedParent> extends BaseAction<T> {
href: string;
onClick?: never;
}

interface ButtonAction<T extends SupportedParent> extends BaseAction<T> {
href?: never;
onClick: () => void;
}

export type Action<T extends SupportedParent> = LinkAction<T> | ButtonAction<T>;
}
62 changes: 5 additions & 57 deletions components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';
import { LogOut, LucideIcon, User, UserRound } from 'lucide-react';
import { signIn, signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
import { NavigationList } from './NavigationList';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Button, ButtonProps } from './ui/button';
import { Button } from './ui/button';
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -19,7 +19,7 @@ export type ProfileProps = {
mobile?: boolean;
};

const links: Profile.Action[] = [
const items: NavigationList.Action<typeof Button | typeof DropdownMenuItem>[] = [
{
label: 'Your Profile',
href: '/profile',
Expand Down Expand Up @@ -56,33 +56,7 @@ export function Profile({ className, mobile = false }: ProfileProps) {
</Avatar>
<span>{session.user.email}</span>
</div>
{links.map((link, index) => {
const baseProps: Pick<ButtonProps, 'variant' | 'className'> = {
variant: link.variant ?? 'ghost',
className: 'gap-4 justify-between',
};

const content = (
<>
{link.label}
{link.icon && <link.icon />}
</>
);

if (link.href) {
return (
<Button key={index} {...baseProps} asChild>
<Link href={link.href}>{content}</Link>
</Button>
);
}

return (
<Button key={index} {...baseProps} onClick={link.onClick}>
{content}
</Button>
);
})}
<NavigationList items={items} as={Button} variant="ghost" />
</>
);
}
Expand All @@ -101,33 +75,7 @@ export function Profile({ className, mobile = false }: ProfileProps) {
<DropdownMenuLabel className="text-md">My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="flex flex-col gap-1">
{links.map((link, index) => {
const baseProps: Pick<DropdownMenuItemProps, 'variant' | 'className'> = {
variant: link.variant,
className: 'text-sm flex justify-between gap-4',
};

const content = (
<>
<span>{link.label}</span>
{link.icon && <link.icon className="size-6" />}
</>
);

if (link.href) {
return (
<DropdownMenuItem {...baseProps} key={index} asChild>
<Link href={link.href}>{content}</Link>
</DropdownMenuItem>
);
}

return (
<DropdownMenuItem {...baseProps} key={index} onClick={link.onClick}>
{content}
</DropdownMenuItem>
);
})}
<NavigationList items={items} as={DropdownMenuItem} />
</div>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
30 changes: 30 additions & 0 deletions components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client"

import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"

import { cn } from "@/lib/utils"

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

const TooltipTrigger = TooltipPrimitive.Trigger

const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@rain-cafe/react-utils": "^1.2.0",
"@rain-cafe/ribbon": "^1.0.3",
"bcrypt": "^5.1.1",
Expand All @@ -31,6 +32,7 @@
"prisma": "^5.8.0",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.0.1",
"sharp": "^0.33.2",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7"
Expand Down

0 comments on commit 3c88b3e

Please sign in to comment.