Skip to content

Commit

Permalink
feat: add theme switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Pettor committed Feb 21, 2024
1 parent 412f794 commit fd21148
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 16 deletions.
13 changes: 11 additions & 2 deletions src/components/library/image-editor/drawer/AppDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { useRef, type ReactElement } from "react";
import { FolderPlusIcon } from "@heroicons/react/24/solid";
import { useOnClickOutside } from "usehooks-ts";
import type { ThemeSwitchProps } from "../../theme-controller/ThemeSwitch";
import { ThemeSwitch } from "../../theme-controller/ThemeSwitch";

export interface AppDrawerProps {
open: boolean;
themeSwitchProps: ThemeSwitchProps;
onClose: () => void;
onNewImage: () => void;
}

export function AppDrawer({ open, onClose, onNewImage }: AppDrawerProps): ReactElement {
export function AppDrawer({ open, themeSwitchProps, onClose, onNewImage }: AppDrawerProps): ReactElement {
const menuRef = useRef<HTMLUListElement>(null);
useOnClickOutside(menuRef, onClose);

Expand All @@ -21,13 +24,19 @@ export function AppDrawer({ open, onClose, onNewImage }: AppDrawerProps): ReactE
ref={menuRef}
className="menu shadow-xl min-h-full w-60 bg-base-100 p-4 text-base text-base-content md:w-80"
>
<span className="p-4 text-xl font-bold">Image Editor</span>
<div className="flex">
<span className="flex flex-1 p-4 text-xl font-bold">Image Editor</span>
<div className="flex items-center flex-row">
<ThemeSwitch {...themeSwitchProps} />
</div>
</div>
<li onClick={onNewImage}>
<a>
<FolderPlusIcon className="h-6 w-6" />
New Image
</a>
</li>
<div className="flex flex-1"></div>
</ul>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function ToolbarTools(): ReactElement {
<div className="divider divider-horizontal w-0 mx-0" />
<div className="tooltip tooltip-bottom mt-1" data-tip={showFitScreen ? "Actual size" : "Fit to window"}>
<button className="btn btn-square btn-ghost swap btn-sm" onClick={adjustZoom}>
{showFitScreen ? <FitViewIcon /> : <FullscreenIcon />}
{showFitScreen ? <FullscreenIcon /> : <FitViewIcon />}
</button>
</div>
<div className="divider divider-horizontal w-0 mx-0" />
Expand Down
24 changes: 24 additions & 0 deletions src/components/library/theme-controller/ThemeSwitch.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ThemeSwitch as Component } from "./ThemeSwitch";
import type { ThemeSwitchProps as ComponentProps } from "./ThemeSwitch";

const meta = {
component: Component,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
title: "Library/ThemeSwitch",
} satisfies Meta<typeof Component>;

export default meta;
type Story = StoryObj<typeof meta>;

const commonProps = {
mode: "light",
onSwitch: () => console.log("Switched"),
} satisfies ComponentProps;

export const Standard = {
args: commonProps,
} satisfies Story;
50 changes: 50 additions & 0 deletions src/components/library/theme-controller/ThemeSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ReactElement } from "react";
import type { ThemeMode } from "./ThemeSwitcherClasses";

export interface ThemeSwitchProps {
mode: ThemeMode;
onSwitch: () => void;
}

export function ThemeSwitch({ mode, onSwitch }: ThemeSwitchProps): ReactElement {
return (
<label className="cursor-pointer grid place-items-center">
<input
type="checkbox"
checked={mode === "dark"}
value={mode}
onChange={onSwitch}
className="toggle theme-controller bg-base-content row-start-1 col-start-1 col-span-2"
/>
<svg
className="col-start-1 row-start-1 stroke-base-100 fill-base-100"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
</svg>
<svg
className="col-start-2 row-start-1 stroke-base-100 fill-base-100"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</label>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ThemeMode = "light" | "dark";
46 changes: 46 additions & 0 deletions src/components/library/theme-controller/UseThemeSwitcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useLayoutEffect, useState } from "react";
import { useLocalStorage } from "usehooks-ts";
import type { ThemeSwitchProps } from "./ThemeSwitch";
import type { ThemeMode } from "./ThemeSwitcherClasses";

export function useThemeSwitcher(): ThemeSwitchProps {
const [mediaQuery] = useState(() => window.matchMedia("(prefers-color-scheme: dark)"));
const [themeLocalStorage, setThemeLocalStorage] = useLocalStorage<ThemeMode>("theme", () => {
if (mediaQuery.matches) {
return "dark";
} else {
return "light";
}
});

useLayoutEffect(() => {
if (themeLocalStorage) {
return;
}

if (mediaQuery.matches) {
setThemeLocalStorage("dark");
} else {
setThemeLocalStorage("light");
}
}, [mediaQuery, setThemeLocalStorage, themeLocalStorage]);

useEffect(() => {
if (themeLocalStorage === "dark") {
document.documentElement.classList.add("dark");
document.querySelector("html")?.setAttribute("data-theme", "dark");
} else {
document.documentElement.classList.remove("dark");
document.querySelector("html")?.setAttribute("data-theme", "light");
}
}, [themeLocalStorage]);

function setTheme(): void {
setThemeLocalStorage((theme) => (theme === "light" ? "dark" : "light"));
}

return {
mode: themeLocalStorage,
onSwitch: setTheme,
};
}
34 changes: 22 additions & 12 deletions src/components/views/HomeView.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import type { ReactElement } from "react";
import { Dropzone } from "../library/dropzone/Dropzone";
import { ThemeSwitch, type ThemeSwitchProps } from "../library/theme-controller/ThemeSwitch";
import { BasicLayout } from "~/components/layout/BasicLayout";

export interface HomeViewProps {
themeSwitchProps: ThemeSwitchProps;
onDrop: (acceptedFiles: File[]) => void;
}

export function HomeView({ onDrop }: HomeViewProps): ReactElement {
export function HomeView({ themeSwitchProps, onDrop }: HomeViewProps): ReactElement {
return (
<BasicLayout container footer>
<div className="hero-content text-center">
<div className="max-w-md">
<h1 className="inline-block bg-gradient-to-r from-primary to-secondary bg-clip-text p-2 text-3xl font-bold text-transparent md:text-5xl lg:text-7xl">
Pixi Image Editor
</h1>
<p className="py-6">
This is a image editor built using <b className="text-secondary">PixiJS</b> and React. You can upload an
image to get started.
</p>
<div className="flex h-full w-full items-center justify-center">
<Dropzone onDrop={onDrop} />
<div className="w-full flex items-center justify-center">
<div className="flex flex-1" />
<div className="relative top-5 right-5 sm:top-10">
<ThemeSwitch {...themeSwitchProps} />
</div>
</div>
<div className="flex flex-1">
<div className="hero-content text-center">
<div className="max-w-md">
<h1 className="inline-block bg-gradient-to-r from-primary to-secondary bg-clip-text p-2 text-3xl font-bold text-transparent md:text-5xl lg:text-7xl">
Pixi Image Editor
</h1>
<p className="py-6">
This is a image editor built using <b className="text-secondary">PixiJS</b> and React. You can upload an
image to get started.
</p>
<div className="flex h-full w-full items-center justify-center">
<Dropzone onDrop={onDrop} />
</div>
</div>
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { ReactElement } from "react";
import { useNavigate } from "react-router-dom";
import { useHomePage } from "./UseHomePage";
import { useThemeSwitcher } from "~/components/library/theme-controller/UseThemeSwitcher";
import { HomeView } from "~/components/views/HomeView";

export function HomePage(): ReactElement {
const navigate = useNavigate();
const { setDroppedFile } = useHomePage();
const themeSwitchProps = useThemeSwitcher();

function handleOnDrop(acceptedFiles: File[]): void {
if (acceptedFiles.length > 1 || acceptedFiles.length === 0) {
Expand All @@ -23,5 +25,5 @@ export function HomePage(): ReactElement {
navigate(`/editor`);
}

return <HomeView onDrop={handleOnDrop} />;
return <HomeView onDrop={handleOnDrop} themeSwitchProps={themeSwitchProps} />;
}
3 changes: 3 additions & 0 deletions src/pages/image-editor/ImageEditorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactElement } from "react";
import { useNavigate } from "react-router-dom";
import { ImageEditor } from "~/components/library/image-editor/Editor";
import { useThemeSwitcher } from "~/components/library/theme-controller/UseThemeSwitcher";
import { ErrorView } from "~/components/views/ErrorView";
import { LoadingView } from "~/components/views/LoadingView";

Expand All @@ -10,6 +11,7 @@ export interface ImageEditorPageProps {

export function ImageEditorPage({ url }: ImageEditorPageProps): ReactElement {
const navigate = useNavigate();
const themeSwitchProps = useThemeSwitcher();

function handleOnNewImage(): void {
navigate(`/`);
Expand All @@ -20,6 +22,7 @@ export function ImageEditorPage({ url }: ImageEditorPageProps): ReactElement {
url={url}
appdrawerProps={{
onNewImage: handleOnNewImage,
themeSwitchProps,
}}
LoaderComponent={() => <LoadingView />}
ErrorComponent={() => <ErrorView />}
Expand Down
6 changes: 6 additions & 0 deletions src/pages/not-found/NotFoundRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { ReactElement } from "react";
import { redirect } from "react-router-dom";
import { NotFoundPage } from "./NotFoundPage";
import { ErrorView } from "~/components/views/ErrorView";

export function loader(): Response {
// For now redirect to root
return redirect("/");
}

export function Component(): ReactElement {
return <NotFoundPage />;
}
Expand Down

0 comments on commit fd21148

Please sign in to comment.