add settings and sidebar toggle, refactor a bit

This commit is contained in:
2024-07-07 15:34:22 +02:00
parent c1a31640a3
commit d621b18629
18 changed files with 571 additions and 167 deletions

Binary file not shown.

View File

@@ -12,9 +12,11 @@
}, },
"dependencies": { "dependencies": {
"@it-incubator/prettier-config": "^0.1.2", "@it-incubator/prettier-config": "^0.1.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@tanstack/react-query": "^5.50.1", "@tanstack/react-query": "^5.50.1",
"@tanstack/react-router": "^1.43.12", "@tanstack/react-router": "^1.43.12",
"@tanstack/react-table": "^8.19.2", "@tanstack/react-table": "^8.19.2",
@@ -26,7 +28,8 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8" "zod": "^3.23.8",
"zustand": "^4.5.4"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3", "@biomejs/biome": "1.8.3",

View File

@@ -14,6 +14,7 @@ import {
} from "@/components/ui"; } from "@/components/ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTableColumnsQuery, useTableDataQuery } from "@/services/db"; import { useTableColumnsQuery, useTableDataQuery } from "@/services/db";
import { useSettingsStore } from "@/state";
import { import {
type ColumnDef, type ColumnDef,
type OnChangeFn, type OnChangeFn,
@@ -55,6 +56,9 @@ export const DataTable = ({
onPageIndexChange: (pageIndex: number) => void; onPageIndexChange: (pageIndex: number) => void;
onPageSizeChange: (pageSize: number) => void; onPageSizeChange: (pageSize: number) => void;
}) => { }) => {
const formatDates = useSettingsStore.use.formatDates();
const showImagesPreview = useSettingsStore.use.showImagesPreview();
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
@@ -108,10 +112,10 @@ export const DataTable = ({
cell: ({ row }) => { cell: ({ row }) => {
const value = row.getValue(column_name) as any; const value = row.getValue(column_name) as any;
let finalValue = value; let finalValue = value;
if (udt_name === "timestamp") { if (formatDates && udt_name === "timestamp") {
finalValue = new Date(value as string).toLocaleString(); finalValue = new Date(value as string).toLocaleString();
} }
if (typeof value === "string" && isUrl(value)) { if (showImagesPreview && typeof value === "string" && isUrl(value)) {
const isImage = isImageUrl(value); const isImage = isImageUrl(value);
return ( return (
<a <a
@@ -120,7 +124,9 @@ export const DataTable = ({
className={cn("hover:underline")} className={cn("hover:underline")}
rel="noreferrer" rel="noreferrer"
> >
<div className={"flex items-center break-all gap-4"}> <div
className={"flex items-center justify-between break-all gap-4"}
>
{value} {value}
{isImage && ( {isImage && (
<img <img
@@ -142,7 +148,7 @@ export const DataTable = ({
); );
}, },
})) as ColumnDef<any>[]; })) as ColumnDef<any>[];
}, [details]); }, [details, formatDates, showImagesPreview]);
const table = useReactTable({ const table = useReactTable({
data: data?.data ?? [], data: data?.data ?? [],

View File

@@ -0,0 +1,132 @@
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Input,
Switch,
Table,
TableBody,
TableCell,
TableRow,
} from "@/components/ui";
import { useSettingsStore } from "@/state";
import { Settings, Trash } from "lucide-react";
import type { FormEventHandler } from "react";
export function SettingsDialog() {
const addPaginationOption = useSettingsStore.use.addPaginationOption();
const removePaginationOption = useSettingsStore.use.removePaginationOption();
const paginationOptions = useSettingsStore.use.paginationOptions();
const formatDates = useSettingsStore.use.formatDates();
const setFormatDates = useSettingsStore.use.setFormatDates();
const showImagesPreview = useSettingsStore.use.showImagesPreview();
const setShowImagesPreview = useSettingsStore.use.setShowImagesPreview();
const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const stringValue = formData.get("option");
if (!stringValue || typeof stringValue !== "string") {
return;
}
const value = Number.parseInt(stringValue, 10);
if (Number.isNaN(value)) {
return;
}
addPaginationOption(value);
e.currentTarget.reset();
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size={"icon"}>
<Settings className={"size-[1.2rem]"} />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[1025px]">
<DialogHeader>
<DialogTitle>Global Settings</DialogTitle>
<DialogDescription>
Update global settings for the app. Changes will be applied
immediately and persisted to local storage.
</DialogDescription>
</DialogHeader>
<div className={"space-y-6"}>
<div>
<h2>Pagination options</h2>
<p>Add or remove options for the amount of rows per page</p>
<Table className={"w-auto"}>
<TableBody>
{paginationOptions.map((option) => (
<TableRow key={option} className={"border-none"}>
<TableCell className={"px-2 py-1 text-end"}>
{option}
</TableCell>
<TableCell className={"px-2 py-1"}>
<Button
variant={"ghost"}
size={"iconSm"}
onClick={() => removePaginationOption(option)}
title={"Delete option"}
>
<Trash className={"size-4"} />
<span className={"sr-only"}>Delete option</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<form className={"mt-4 flex gap-4"} onSubmit={handleSubmit}>
<Input name={"option"} type={"number"} className={"max-w-28"} />
<Button>Add</Button>
</form>
</div>
<div>
<h2>Automatically format dates</h2>
<p>
When turned on, will show timestamp cells in tables in a human
readable format
</p>
<Switch
checked={formatDates}
onCheckedChange={setFormatDates}
className={"mt-4"}
/>
</div>
<div>
<h2>Show previews for images</h2>
<p>
When turned on, will automatically detect image URL's in tables
and add a preview alongside.
</p>
<p>
Might significantly increase load on you CDN, use with caution.
</p>
<Switch
checked={showImagesPreview}
onCheckedChange={setShowImagesPreview}
className={"mt-4"}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button">Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -6,6 +6,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui"; } from "@/components/ui";
import { useSettingsStore } from "@/state";
import type { Table } from "@tanstack/react-table"; import type { Table } from "@tanstack/react-table";
import { import {
ChevronLeftIcon, ChevronLeftIcon,
@@ -21,6 +22,7 @@ interface DataTablePaginationProps<TData> {
export function DataTablePagination<TData>({ export function DataTablePagination<TData>({
table, table,
}: DataTablePaginationProps<TData>) { }: DataTablePaginationProps<TData>) {
const paginationOptions = useSettingsStore.use.paginationOptions();
return ( return (
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"> <div className="flex-1 text-sm text-muted-foreground">
@@ -40,7 +42,7 @@ export function DataTablePagination<TData>({
<SelectValue placeholder={table.getState().pagination.pageSize} /> <SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">
{[10, 20, 30, 40, 50, 1000].map((pageSize) => ( {paginationOptions.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}> <SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize} {pageSize}
</SelectItem> </SelectItem>

View File

@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -1,7 +1,9 @@
export * from "./button"; export * from "./button";
export * from "./data-table-pagination"; export * from "./data-table-pagination";
export * from "./dialog";
export * from "./dropdown-menu"; export * from "./dropdown-menu";
export * from "./input";
export * from "./mode-toggle"; export * from "./mode-toggle";
export * from "./select"; export * from "./select";
export * from "./switch";
export * from "./table"; export * from "./table";
export * from "./theme-provider";

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -6,11 +6,31 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
useTheme,
} from "@/components/ui"; } from "@/components/ui";
import { useUiStore } from "@/state";
import { useLayoutEffect } from "react";
export function ModeToggle() { export function ModeToggle() {
const { setTheme } = useTheme(); const theme = useUiStore.use.theme();
const setTheme = useUiStore.use.setTheme();
useLayoutEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
return ( return (
<DropdownMenu> <DropdownMenu>

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -1,71 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider')
return context
}

View File

@@ -3,6 +3,9 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
input[type=number] {
-moz-appearance:textfield;
}
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
@@ -69,21 +72,28 @@
@layer base { @layer base {
:root { :root {
text-underline-position: under; text-underline-position: under;
--sidebar-width: 264px;
} }
.sidebar-closed {
--sidebar-width: 0;
}
.grid-rows-layout { .grid-rows-layout {
grid-template-rows: 60px 1fr; grid-template-rows: 60px 1fr;
} }
.grid-cols-layout { .grid-cols-layout {
grid-template-columns: 264px 1fr; grid-template-columns: var(--sidebar-width) 1fr;
} }
.max-w-layout { .max-w-layout {
max-width: calc(100vw - 264px); max-width: calc(100vw - var(--sidebar-width));
} }
.w-layout { .w-layout {
width: calc(100vw - 264px); width: calc(100vw - var(--sidebar-width));
} }
.resizer { .resizer {
position: absolute; position: absolute;

View File

@@ -0,0 +1,17 @@
import type { StoreApi, UseBoundStore } from "zustand";
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S,
) => {
const store = _store as WithSelectors<typeof _store>;
store.use = {};
for (const k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}
return store;
};

View File

@@ -2,7 +2,6 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react"; import { StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css"; import "./index.css";
import { ThemeProvider } from "@/components/ui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Import the generated route tree // Import the generated route tree
@@ -25,15 +24,13 @@ if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<StrictMode> <StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <ReactQueryDevtools
<ReactQueryDevtools initialIsOpen={false}
initialIsOpen={false} buttonPosition={"bottom-left"}
buttonPosition={"top-right"} />
/> <RouterProvider router={router} />
<RouterProvider router={router} /> </QueryClientProvider>
</QueryClientProvider>
</ThemeProvider>
</StrictMode>, </StrictMode>,
); );
} }

View File

@@ -1,4 +1,6 @@
import { SettingsDialog } from "@/components/settings-dialog";
import { import {
Button,
ModeToggle, ModeToggle,
Select, Select,
SelectContent, SelectContent,
@@ -9,6 +11,7 @@ import {
} from "@/components/ui"; } from "@/components/ui";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useDatabasesListQuery, useTablesListQuery } from "@/services/db"; import { useDatabasesListQuery, useTablesListQuery } from "@/services/db";
import { useUiStore } from "@/state";
import { import {
Link, Link,
Outlet, Outlet,
@@ -17,15 +20,23 @@ import {
useParams, useParams,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools"; import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { Database, Rows3, Table2 } from "lucide-react"; import {
Database,
PanelLeft,
PanelLeftClose,
Rows3,
Table2,
} from "lucide-react";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: Root, component: Root,
}); });
function Root() { function Root() {
const { data } = useDatabasesListQuery(); const showSidebar = useUiStore.use.showSidebar();
const toggleSidebar = useUiStore.use.toggleSidebar();
const { data } = useDatabasesListQuery();
const params = useParams({ strict: false }); const params = useParams({ strict: false });
const dbName = params.dbName ?? ""; const dbName = params.dbName ?? "";
const navigate = useNavigate({ from: Route.fullPath }); const navigate = useNavigate({ from: Route.fullPath });
@@ -38,81 +49,103 @@ function Root() {
return ( return (
<> <>
<div className="h-screen grid grid-rows-layout grid-cols-layout"> <div
<header className="p-2 flex gap-2 border-b items-center col-span-full"> className={cn(
<ModeToggle /> "h-screen grid grid-rows-layout grid-cols-layout",
<Link to="/" className="[&.active]:font-bold"> !showSidebar && "sidebar-closed",
Home )}
</Link> >
<header className="p-2 justify-between flex gap-2 border-b items-center col-span-full">
<div className={"flex items-center gap-2"}>
<Button variant="outline" size="icon" onClick={toggleSidebar}>
{showSidebar ? (
<PanelLeftClose className={"size-[1.2rem]"} />
) : (
<PanelLeft className={"size-[1.2rem]"} />
)}
</Button>
<Link to="/" className="[&.active]:font-bold">
Home
</Link>
</div>
<div className={"flex items-center gap-2"}>
<ModeToggle />
<SettingsDialog />
</div>
</header> </header>
<aside className={"p-3"}> <aside className={"p-3"}>
<Select value={dbName} onValueChange={handleSelectedDb}> {showSidebar && (
<SelectTrigger className="w-full"> <>
<SelectValue placeholder="Database" /> <Select value={dbName} onValueChange={handleSelectedDb}>
</SelectTrigger> <SelectTrigger className="w-full">
<SelectContent> <SelectValue placeholder="Select a Database" />
{data?.map((db) => { </SelectTrigger>
return ( <SelectContent>
<SelectItem value={db} key={db}> {data?.map((db) => {
{db} return (
</SelectItem> <SelectItem value={db} key={db}>
); {db}
})} </SelectItem>
</SelectContent> );
</Select> })}
<nav className="flex flex-col gap-1 mt-4"> </SelectContent>
{dbName && ( </Select>
<Link <nav className="flex flex-col gap-1 mt-4">
to={"/db/$dbName/tables"} {dbName && (
params={{ dbName }} <Link
activeOptions={{ exact: true }} to={"/db/$dbName/tables"}
className={cn( params={{ dbName }}
"flex items-center gap-2 rounded py-1.5 pl-1.5", activeOptions={{ exact: true }}
"hover:bg-muted", className={cn(
"[&.active]:bg-muted [&.active]:font-semibold", "flex items-center gap-2 rounded py-1.5 pl-1.5",
"hover:bg-muted",
"[&.active]:bg-muted [&.active]:font-semibold",
)}
>
<Database className={"size-4"} /> {dbName}
</Link>
)} )}
> {tables?.map((table) => {
<Database className={"size-4"} /> {dbName} return (
</Link> <div
)} key={table.table_name}
{tables?.map((table) => { className={cn(
return ( "flex items-center gap-2 px-2.5 rounded py-1.5 justify-between w-full",
<div )}
key={table.table_name} >
className={cn( <Link
"flex items-center gap-2 px-2.5 rounded py-1.5 justify-between w-full", className={cn(
)} "w-full flex gap-2 items-center",
> "hover:underline",
<Link "[&.active]:font-semibold",
className={cn( )}
"w-full flex gap-2 items-center", to={"/db/$dbName/tables/$tableName"}
"hover:underline", params={{ tableName: table.table_name, dbName: dbName }}
"[&.active]:font-semibold", >
)} <Table2 className={"size-4 shrink-0"} />
to={"/db/$dbName/tables/$tableName"} {table.table_name}
params={{ tableName: table.table_name, dbName: dbName }} </Link>
> <Link
<Table2 className={"size-4 shrink-0"} /> className={cn(
{table.table_name} "hover:underline shrink-0",
</Link> buttonVariants({ variant: "ghost", size: "iconSm" }),
<Link "[&.active]:bg-muted",
className={cn( )}
"hover:underline shrink-0", title={"Explore Data"}
buttonVariants({ variant: "ghost", size: "iconSm" }), aria-label={"Explore Data"}
"[&.active]:bg-muted", to={"/db/$dbName/tables/$tableName/data"}
)} params={{ tableName: table.table_name, dbName: dbName }}
title={"Explore Data"} search={{ pageIndex: 0, pageSize: 10 }}
aria-label={"Explore Data"} >
to={"/db/$dbName/tables/$tableName/data"} <Rows3 className={"size-4 shrink-0"} />
params={{ tableName: table.table_name, dbName: dbName }} </Link>
search={{ pageIndex: 0, pageSize: 10 }} </div>
> );
<Rows3 className={"size-4 shrink-0"} /> })}
</Link> </nav>
</div> </>
); )}
})}
</nav>
</aside> </aside>
<Outlet /> <Outlet />
</div> </div>

View File

@@ -0,0 +1,2 @@
export * from "./settings-store";
export * from "./ui-store";

View File

@@ -0,0 +1,47 @@
import { createSelectors } from "@/lib/create-selectors";
import { create } from "zustand";
import { persist } from "zustand/middleware";
type SettingsState = {
paginationOptions: Array<number>;
addPaginationOption: (option: number) => void;
removePaginationOption: (option: number) => void;
formatDates: boolean;
setFormatDates: (value: boolean) => void;
showImagesPreview: boolean;
setShowImagesPreview: (value: boolean) => void;
};
const useSettingsStoreBase = create<SettingsState>()(
persist(
(set) => ({
paginationOptions: [10, 20, 50, 100],
addPaginationOption: (option) =>
set((state) => {
if (state.paginationOptions.includes(option)) {
return state;
}
return {
paginationOptions: [...state.paginationOptions, option].sort(
(a, b) => a - b,
),
};
}),
removePaginationOption: (option) =>
set((state) => ({
paginationOptions: state.paginationOptions.filter(
(o) => o !== option,
),
})),
formatDates: true,
setFormatDates: (value) => set({ formatDates: value }),
showImagesPreview: true,
setShowImagesPreview: (value) => set({ showImagesPreview: value }),
}),
{
name: "settings-storage",
},
),
);
export const useSettingsStore = createSelectors(useSettingsStoreBase);

View File

@@ -0,0 +1,32 @@
import { createSelectors } from "@/lib/create-selectors";
import { create } from "zustand";
import { persist } from "zustand/middleware";
type Theme = "dark" | "light" | "system";
type UiState = {
showSidebar: boolean;
toggleSidebar: () => void;
theme: Theme;
setTheme: (theme: Theme) => void;
};
const useUiStoreBase = create<UiState>()(
persist(
(set) => {
return {
showSidebar: false,
toggleSidebar: () => {
set((state) => ({ showSidebar: !state.showSidebar }));
},
theme: "system",
setTheme: (theme) => set({ theme }),
};
},
{
name: "ui-storage",
},
),
);
export const useUiStore = createSelectors(useUiStoreBase);