Skip to content

Commit

Permalink
feat: add dark mode toggle (#34)
Browse files Browse the repository at this point in the history
* feat: add dark mode toggle

* refactor: fix tailwind classes

* refactor: pr updates

* updates
  • Loading branch information
drewradcliff authored Aug 26, 2024
1 parent 828fa48 commit a4ed5ec
Show file tree
Hide file tree
Showing 9 changed files with 667 additions and 46 deletions.
8 changes: 4 additions & 4 deletions app/components/field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export function Field() {
}, [size]);

return !plots || sessionState === "dead" ? (
<div className="absolute bg-white h-[100dvh] w-[100dvw] z-10">
<div className="absolute h-[100dvh] w-[100dvw] z-10 bg-white dark:bg-slate-950">
<div className="h-full flex items-center justify-center">
{sessionState === "dead" ? (
<Skull className="h-16 w-16 text-red-600" />
<Skull className="h-16 w-16 text-red-600 dark:text-red-300" />
) : (
<div
className="grid animate-pulse"
Expand All @@ -59,7 +59,7 @@ export function Field() {
{Array.from({ length: 9 }, (_, index) => (
<div
key={index}
className="bg-gray-100"
className="bg-slate-100 dark:bg-slate-600"
style={{ height: `${GRID_SIZE}rem`, width: `${GRID_SIZE}rem` }}
/>
))}
Expand All @@ -69,7 +69,7 @@ export function Field() {
</div>
) : (
<div
className="max-w-full select-none overflow-auto bg-white flex-1"
className="max-w-full select-none overflow-auto flex-1"
onContextMenu={(event) => event.preventDefault()}
ref={parentRef}
>
Expand Down
10 changes: 7 additions & 3 deletions app/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use client";

import { Percent, UsersIcon } from "lucide-react";
import { ThemeToggle } from "@/components/theme-toggle";
import { useSocketEvent } from "@/hooks/use-socket-event";

export function Header() {
const [clientsCount] = useSocketEvent("clientsCount");
const [exposedPercent] = useSocketEvent("exposedPercent");

return (
<header className="w-[min(var(--field-size),100%)] bg-white px-4 flex justify-between">
<h1 className="text-4xl font-extrabold uppercase italic">mmmines</h1>
<div className="flex flex-col items-end gap-2">
<header className="w-[min(var(--field-size),100%)] px-4 py-1 flex justify-between">
<div className="flex flex-row gap-6">
<h1 className="text-4xl font-extrabold uppercase italic pr-4">
mmmines
</h1>
<div className="flex items-center gap-2">
<span>{clientsCount ?? "⋯"}</span>
<UsersIcon className="h-4 w-4" />
Expand All @@ -20,6 +23,7 @@ export function Header() {
<Percent className="h-4 w-4" />
</div>
</div>
<ThemeToggle />
</header>
);
}
25 changes: 14 additions & 11 deletions app/components/plot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,20 @@ export function Plot({ className, index, state, ...props }: Props) {
}

const classMap = new Map<PlotState, string>([
[1, tw`text-gray-300`],
[2, tw`text-gray-400`],
[3, tw`text-gray-500`],
[4, tw`text-gray-600`],
[5, tw`text-gray-700`],
[6, tw`text-gray-800`],
[7, tw`text-gray-900`],
[8, tw`text-gray-950`],
["mine", tw`bg-red-500 text-gray-800`],
["flagged", tw`bg-gray-100 text-yellow-400`],
["unknown", tw`bg-gray-100`],
[1, tw`text-slate-300 dark:text-slate-700`],
[2, tw`text-slate-400 dark:text-slate-600`],
[3, tw`text-slate-500 dark:text-slate-500`],
[4, tw`text-slate-600 dark:text-slate-400`],
[5, tw`text-slate-700 dark:text-slate-300`],
[6, tw`text-slate-800 dark:text-slate-200`],
[7, tw`text-slate-900 dark:text-slate-100`],
[8, tw`text-slate-950 dark:text-slate-50`],
["mine", tw`bg-red-500 dark:bg-red-400 text-slate-800 dark:text-slate-950`],
[
"flagged",
tw`bg-slate-100 dark:bg-slate-600 text-yellow-400 dark:text-yellow-300`,
],
["unknown", tw`bg-slate-100 dark:bg-slate-600`],
]);

const textMap = new Map<PlotState, string | null>([
Expand Down
38 changes: 38 additions & 0 deletions app/components/theme-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export function ThemeToggle() {
const { setTheme } = useTheme();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-5 w-5 dark:hidden" />
<Moon className="h-5 w-5 hidden dark:block" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
6 changes: 4 additions & 2 deletions app/components/tutorial-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ export function TutorialDialog() {
<div className="flex flex-col items-center text-center py-4">
{currentStep.icon}
<h3 className="text-lg font-semibold mb-2">{currentStep.title}</h3>
<p className="text-sm text-gray-600">{currentStep.description}</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
{currentStep.description}
</p>
</div>
<DialogFooter>
<div className="text-sm text-gray-600 text-center">
<div className="text-sm text-slate-600 dark:text-slate-300 text-center">
Step {step} of {totalSteps}
</div>
<div className="space-x-2 flex">
Expand Down
200 changes: 200 additions & 0 deletions app/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"use client";

import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Dot } from "lucide-react";
import { cn } from "@/utils";

const DropdownMenu = DropdownMenuPrimitive.Root;

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;

const DropdownMenuGroup = DropdownMenuPrimitive.Group;

const DropdownMenuPortal = DropdownMenuPrimitive.Portal;

const DropdownMenuSub = DropdownMenuPrimitive.Sub;

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;

const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-800 dark:data-[state=open]:bg-slate-800",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;

const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;

const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;

const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;

const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className,
)}
checked={!!checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;

const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Dot className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;

const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;

const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;

const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";

export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
15 changes: 9 additions & 6 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { ThemeProvider } from "next-themes";
import { Header } from "@/components/header";
import { SocketProvider } from "@/components/socket-provider";
import "@/globals.css";
Expand All @@ -10,13 +11,15 @@ export const metadata: Metadata = {

export default function RootLayout(props: Readonly<React.PropsWithChildren>) {
return (
<html lang="en" className={GeistSans.variable}>
<body className="min-h-screen bg-background font-sans antialiased">
<html lang="en" className={GeistSans.variable} suppressHydrationWarning>
<body className="min-h-screen bg-white dark:bg-slate-950 font-sans antialiased">
<main className="flex flex-col items-center h-[100dvh]">
<SocketProvider>
<Header />
{props.children}
</SocketProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<SocketProvider>
<Header />
{props.children}
</SocketProvider>
</ThemeProvider>
</main>
</body>
</html>
Expand Down
Loading

0 comments on commit a4ed5ec

Please sign in to comment.