mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-15 00:43:21 +07:00
feat: fix responsive layout, add theme switcher
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, ucfirst } from "@/lib/utils";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
HardDrive,
|
||||
KeySquare,
|
||||
LayoutDashboard,
|
||||
Palette,
|
||||
} from "lucide-react";
|
||||
import { Menu } from "react-daisyui";
|
||||
import { Dropdown, Menu } from "react-daisyui";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import Button from "../ui/button";
|
||||
import { themes } from "@/app/themes";
|
||||
import appStore from "@/stores/app-store";
|
||||
|
||||
const pages = [
|
||||
{ icon: LayoutDashboard, title: "Dashboard", path: "/", exact: true },
|
||||
@@ -19,7 +23,7 @@ const Sidebar = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<aside className="bg-base-100 border-r border-base-300/30 w-[220px] overflow-y-auto">
|
||||
<aside className="bg-base-100 border-r border-base-300/30 w-[80%] md:w-[250px] flex flex-col items-stretch overflow-hidden h-full">
|
||||
<div className="p-4">
|
||||
<img
|
||||
src="https://garagehq.deuxfleurs.fr/images/garage-logo.svg"
|
||||
@@ -28,7 +32,8 @@ const Sidebar = () => {
|
||||
/>
|
||||
<p className="text-sm font-medium text-center">WebUI</p>
|
||||
</div>
|
||||
<Menu className="gap-y-1">
|
||||
|
||||
<Menu className="gap-y-1 flex-1 overflow-y-auto">
|
||||
{pages.map((page) => {
|
||||
const isActive = page.exact
|
||||
? pathname === page.path
|
||||
@@ -50,6 +55,22 @@ const Sidebar = () => {
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
|
||||
<Dropdown className="my-2 mx-4" vertical="top">
|
||||
<Dropdown.Toggle button={false}>
|
||||
<Button icon={Palette} color="ghost">
|
||||
Theme
|
||||
</Button>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu className="max-h-[200px] overflow-y-auto">
|
||||
{themes.map((theme) => (
|
||||
<Dropdown.Item key={theme} onClick={() => appStore.setTheme(theme)}>
|
||||
{ucfirst(theme)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import appStore from "@/stores/app-store";
|
||||
import { useEffect } from "react";
|
||||
import { useStore } from "zustand";
|
||||
|
||||
const ThemeProvider = () => {
|
||||
const theme = useStore(appStore, (i) => i.theme);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ThemeProvider;
|
||||
@@ -1,29 +1,46 @@
|
||||
import { PageContext } from "@/context/page-context";
|
||||
import { Suspense, useContext } from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { Suspense, useContext, useEffect } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import Sidebar from "../containers/sidebar";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { ArrowLeft, MenuIcon } from "lucide-react";
|
||||
import Button from "../ui/button";
|
||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||
import { Drawer } from "react-daisyui";
|
||||
|
||||
const MainLayout = () => {
|
||||
const sidebar = useDisclosure();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebar.isOpen) {
|
||||
sidebar.onClose();
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-stretch h-screen overflow-hidden">
|
||||
<Sidebar />
|
||||
<Drawer
|
||||
open={sidebar.isOpen}
|
||||
onClickOverlay={sidebar.onClose}
|
||||
className="md:drawer-open h-screen"
|
||||
side={<Sidebar />}
|
||||
contentClassName="flex flex-col overflow-hidden"
|
||||
>
|
||||
<Header onSidebarOpen={sidebar.onOpen} />
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Header />
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
||||
<Suspense>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
||||
<Suspense>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
type HeaderProps = {
|
||||
onSidebarOpen: () => void;
|
||||
};
|
||||
|
||||
const Header = ({ onSidebarOpen }: HeaderProps) => {
|
||||
const page = useContext(PageContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -35,11 +52,18 @@ const Header = () => {
|
||||
onClick={() => navigate(page.prev!, { replace: true })}
|
||||
color="ghost"
|
||||
shape="circle"
|
||||
className="-ml-4"
|
||||
className="-mx-2"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
) : null}
|
||||
) : (
|
||||
<Button
|
||||
icon={MenuIcon}
|
||||
color="ghost"
|
||||
className="md:hidden -mx-2"
|
||||
onClick={onSidebarOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h1 className="text-xl flex-1 truncate">{page?.title || "Dashboard"}</h1>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ComponentPropsWithoutRef } from "react";
|
||||
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import BaseSelect from "react-select";
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
@@ -8,11 +8,12 @@ type Props = ComponentPropsWithoutRef<typeof BaseSelect> & {
|
||||
onCreateOption?: (inputValue: string) => void;
|
||||
};
|
||||
|
||||
const Select = ({ creatable, ...props }: Props) => {
|
||||
const Select = forwardRef<any, Props>(({ creatable, ...props }, ref) => {
|
||||
const Comp = creatable ? Creatable : BaseSelect;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
unstyled
|
||||
classNames={{
|
||||
control: (p) =>
|
||||
@@ -42,6 +43,6 @@ const Select = ({ creatable, ...props }: Props) => {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default Select;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Toggle as BaseToggle } from "react-daisyui";
|
||||
import FormControl from "./form-control";
|
||||
import { FieldValues } from "react-hook-form";
|
||||
|
||||
type ToggleProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof BaseToggle>,
|
||||
"form"
|
||||
> & { label?: string };
|
||||
|
||||
const Toggle = forwardRef<HTMLInputElement, ToggleProps>(
|
||||
({ label, ...props }, ref) => {
|
||||
return (
|
||||
<label className="inline-flex justify-start label label-text gap-2 cursor-pointer">
|
||||
<BaseToggle ref={ref} {...props} />
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type ToggleFieldProps<T extends FieldValues> = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof FormControl<T>>,
|
||||
"render"
|
||||
> &
|
||||
ToggleProps & {
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export const ToggleField = <T extends FieldValues>({
|
||||
form,
|
||||
name,
|
||||
title,
|
||||
className,
|
||||
inputClassName,
|
||||
...props
|
||||
}: ToggleFieldProps<T>) => {
|
||||
return (
|
||||
<FormControl
|
||||
form={form}
|
||||
name={name}
|
||||
title={title}
|
||||
className={className}
|
||||
render={(field) => (
|
||||
<Toggle {...props} {...field} className={inputClassName} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toggle;
|
||||
Reference in New Issue
Block a user