mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
admin ui: started color centralization + made sidebar resizable
This commit is contained in:
@@ -18,13 +18,7 @@ const Base: React.FC<AlertProps> = ({
|
||||
...props
|
||||
}) =>
|
||||
visible ? (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center dark:bg-amber-300/20 bg-amber-200 p-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div {...props} className={twMerge("flex flex-row items-center p-4", className)}>
|
||||
<p>
|
||||
{title && <b>{title}: </b>}
|
||||
{message || children}
|
||||
@@ -33,19 +27,19 @@ const Base: React.FC<AlertProps> = ({
|
||||
) : null;
|
||||
|
||||
const Warning: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-amber-300/20 bg-amber-200", className)} />
|
||||
<Base {...props} className={twMerge("bg-warning text-warning-foreground", className)} />
|
||||
);
|
||||
|
||||
const Exception: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-red-950 bg-red-100", className)} />
|
||||
<Base {...props} className={twMerge("bg-error text-error-foreground", className)} />
|
||||
);
|
||||
|
||||
const Success: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-green-950 bg-green-100", className)} />
|
||||
<Base {...props} className={twMerge("bg-success text-success-foreground", className)} />
|
||||
);
|
||||
|
||||
const Info: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-blue-950 bg-blue-100", className)} />
|
||||
<Base {...props} className={twMerge("bg-info text-info-foreground", className)} />
|
||||
);
|
||||
|
||||
export const Alert = {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getBrowser } from "core/utils";
|
||||
import type { Field } from "data";
|
||||
import { Switch as RadixSwitch } from "radix-ui";
|
||||
import {
|
||||
type ChangeEventHandler,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ElementType,
|
||||
forwardRef,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb";
|
||||
import { TbCalendar, TbChevronDown, TbEye, TbEyeOff, TbInfoCircle } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
@@ -89,7 +88,7 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none",
|
||||
"bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full",
|
||||
disabledOrReadonly && "bg-muted/50 text-primary/50",
|
||||
!disabledOrReadonly &&
|
||||
"focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all",
|
||||
@@ -99,6 +98,40 @@ export const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>
|
||||
);
|
||||
});
|
||||
|
||||
export const TypeAwareInput = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
(props, ref) => {
|
||||
if (props.type === "password") {
|
||||
return <Password {...props} ref={ref} />;
|
||||
}
|
||||
|
||||
return <Input {...props} ref={ref} />;
|
||||
},
|
||||
);
|
||||
|
||||
export const Password = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
(props, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
function handleToggle() {
|
||||
setVisible((v) => !v);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Input {...props} type={visible ? "text" : "password"} className="w-full" ref={ref} />
|
||||
<div className="absolute right-3 top-0 bottom-0 flex items-center">
|
||||
<IconButton
|
||||
Icon={visible ? TbEyeOff : TbEye}
|
||||
onClick={handleToggle}
|
||||
variant="ghost"
|
||||
className="opacity-70"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JsonSchema } from "json-schema-library";
|
||||
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
|
||||
import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import ErrorBoundary from "ui/components/display/ErrorBoundary";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
@@ -13,6 +13,7 @@ export type FieldProps = {
|
||||
onChange?: (e: ChangeEvent<any>) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
inputProps?: Partial<FieldComponentProps>;
|
||||
} & Omit<FieldwrapperProps, "children" | "schema">;
|
||||
|
||||
export const Field = (props: FieldProps) => {
|
||||
@@ -31,7 +32,14 @@ const fieldErrorBoundary =
|
||||
</Pre>
|
||||
);
|
||||
|
||||
const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props }: FieldProps) => {
|
||||
const FieldImpl = ({
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
required: _required,
|
||||
inputProps,
|
||||
...props
|
||||
}: FieldProps) => {
|
||||
const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name);
|
||||
const required = typeof _required === "boolean" ? _required : ctx.required;
|
||||
//console.log("Field", { name, path, schema });
|
||||
@@ -64,6 +72,7 @@ const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props
|
||||
return (
|
||||
<FieldWrapper name={name} required={required} schema={schema} {...props}>
|
||||
<FieldComponent
|
||||
{...inputProps}
|
||||
schema={schema}
|
||||
name={name}
|
||||
required={required}
|
||||
@@ -81,10 +90,12 @@ export const Pre = ({ children }) => (
|
||||
</pre>
|
||||
);
|
||||
|
||||
export const FieldComponent = ({
|
||||
schema,
|
||||
..._props
|
||||
}: { schema: JsonSchema } & ComponentPropsWithoutRef<"input">) => {
|
||||
export type FieldComponentProps = {
|
||||
schema: JsonSchema;
|
||||
render?: (props: Omit<FieldComponentProps, "render">) => ReactNode;
|
||||
} & ComponentPropsWithoutRef<"input">;
|
||||
|
||||
export const FieldComponent = ({ schema, render, ..._props }: FieldComponentProps) => {
|
||||
const { value } = useFormValue(_props.name!, { strict: true });
|
||||
if (!isTypeSchema(schema)) return null;
|
||||
const props = {
|
||||
@@ -97,6 +108,8 @@ export const FieldComponent = ({
|
||||
: "",
|
||||
};
|
||||
|
||||
if (render) return render({ schema, ...props });
|
||||
|
||||
if (schema.enum) {
|
||||
return <Formy.Select id={props.name} options={schema.enum} {...(props as any)} />;
|
||||
}
|
||||
@@ -158,5 +171,7 @@ export const FieldComponent = ({
|
||||
}
|
||||
}
|
||||
|
||||
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} {...additional} />;
|
||||
return (
|
||||
<Formy.TypeAwareInput id={props.name} {...props} value={props.value ?? ""} {...additional} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useClickOutside, useHotkeys } from "@mantine/hooks";
|
||||
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
|
||||
import { clampNumber } from "core/utils/numbers";
|
||||
import { throttle } from "lodash-es";
|
||||
import { ScrollArea } from "radix-ui";
|
||||
import {
|
||||
@@ -12,13 +13,20 @@ import {
|
||||
import type { IconType } from "react-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell";
|
||||
import { appShellStore } from "ui/store";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
export function Root({ children }) {
|
||||
export function Root({ children }: { children: React.ReactNode }) {
|
||||
const sidebarWidth = appShellStore((store) => store.sidebarWidth);
|
||||
return (
|
||||
<AppShellProvider>
|
||||
<div data-shell="root" className="flex flex-1 flex-col select-none h-dvh">
|
||||
<div
|
||||
id="app-shell"
|
||||
data-shell="root"
|
||||
className="flex flex-1 flex-col select-none h-dvh"
|
||||
style={{ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AppShellProvider>
|
||||
@@ -80,7 +88,7 @@ export function Main({ children }) {
|
||||
data-shell="main"
|
||||
className={twMerge(
|
||||
"flex flex-col flex-grow w-1 flex-shrink-1",
|
||||
sidebar.open && "md:max-w-[calc(100%-350px)]",
|
||||
sidebar.open && "md:max-w-[calc(100%-var(--sidebar-width))]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -89,47 +97,38 @@ export function Main({ children }) {
|
||||
}
|
||||
|
||||
export function Sidebar({ children }) {
|
||||
const ctx = useAppShell();
|
||||
const open = appShellStore((store) => store.sidebarOpen);
|
||||
const close = appShellStore((store) => store.closeSidebar);
|
||||
const ref = useClickOutside(close, null, [document.getElementById("header")]);
|
||||
const [location] = useLocation();
|
||||
|
||||
const ref = useClickOutside(ctx.sidebar?.handler?.close);
|
||||
const closeHandler = () => {
|
||||
open && close();
|
||||
};
|
||||
|
||||
const onClickBackdrop = useEvent((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ctx?.sidebar?.handler.close();
|
||||
});
|
||||
|
||||
const onEscape = useEvent(() => {
|
||||
if (ctx?.sidebar?.open) {
|
||||
ctx?.sidebar?.handler.close();
|
||||
}
|
||||
});
|
||||
// listen for window location change
|
||||
useEffect(closeHandler, [location]);
|
||||
|
||||
// @todo: potentially has to be added to the root, as modals could be opened
|
||||
useHotkeys([["Escape", onEscape]]);
|
||||
|
||||
if (!ctx) {
|
||||
console.warn("AppShell.Sidebar: missing AppShellContext");
|
||||
return null;
|
||||
}
|
||||
useHotkeys([["Escape", closeHandler]]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
data-shell="sidebar"
|
||||
className="hidden md:flex flex-col basis-[350px] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-muted/10"
|
||||
className="hidden md:flex flex-col basis-[var(--sidebar-width)] flex-shrink-0 flex-grow-0 h-full bg-muted/10"
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
<SidebarResize />
|
||||
<div
|
||||
data-open={ctx?.sidebar?.open}
|
||||
data-open={open}
|
||||
className="absolute w-full md:hidden data-[open=true]:translate-x-0 translate-x-[-100%] transition-transform z-10 backdrop-blur-sm"
|
||||
onClick={onClickBackdrop}
|
||||
>
|
||||
<aside
|
||||
/*ref={ref}*/
|
||||
ref={ref}
|
||||
data-shell="sidebar"
|
||||
className="flex-col w-[350px] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-background"
|
||||
className="flex-col w-[var(--sidebar-width)] flex-shrink-0 flex-grow-0 h-full border-muted border-r bg-background"
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
@@ -138,6 +137,59 @@ export function Sidebar({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
const SidebarResize = () => {
|
||||
const setSidebarWidth = appShellStore((store) => store.setSidebarWidth);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [startWidth, setStartWidth] = useState(0);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
setStartX(e.clientX);
|
||||
setStartWidth(
|
||||
Number.parseInt(
|
||||
getComputedStyle(document.getElementById("app-shell")!)
|
||||
.getPropertyValue("--sidebar-width")
|
||||
.replace("px", ""),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const diff = e.clientX - startX;
|
||||
const newWidth = clampNumber(startWidth + diff, 250, window.innerWidth * 0.5);
|
||||
setSidebarWidth(newWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isResizing, startX, startWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-active={isResizing ? 1 : undefined}
|
||||
className="w-px h-full hidden md:flex bg-muted after:transition-colors relative after:absolute after:inset-0 after:-left-px after:w-[2px] select-none data-[active]:after:bg-sky-400 data-[active]:cursor-col-resize hover:after:bg-sky-400 hover:cursor-col-resize after:z-2"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ touchAction: "none" }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) {
|
||||
return (
|
||||
<h2
|
||||
|
||||
@@ -19,11 +19,11 @@ import { Logo } from "ui/components/display/Logo";
|
||||
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
import { useAppShell } from "ui/layouts/AppShell/use-appshell";
|
||||
import { useNavigate } from "ui/lib/routes";
|
||||
import { useLocation } from "wouter";
|
||||
import { NavLink } from "./AppShell";
|
||||
import { autoFormatString } from "core/utils";
|
||||
import { appShellStore } from "ui/store";
|
||||
|
||||
export function HeaderNavigation() {
|
||||
const [location, navigate] = useLocation();
|
||||
@@ -105,10 +105,9 @@ export function HeaderNavigation() {
|
||||
}
|
||||
|
||||
function SidebarToggler() {
|
||||
const { sidebar } = useAppShell();
|
||||
return (
|
||||
<IconButton size="lg" Icon={sidebar.open ? TbX : TbMenu2} onClick={sidebar.handler.toggle} />
|
||||
);
|
||||
const toggle = appShellStore((store) => store.toggleSidebar);
|
||||
const open = appShellStore((store) => store.sidebarOpen);
|
||||
return <IconButton size="lg" Icon={open ? TbX : TbMenu2} onClick={toggle} />;
|
||||
}
|
||||
|
||||
export function Header({ hasSidebar = true }) {
|
||||
@@ -118,6 +117,7 @@ export function Header({ hasSidebar = true }) {
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
data-shell="header"
|
||||
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
||||
>
|
||||
|
||||
@@ -4,17 +4,28 @@
|
||||
|
||||
#bknd-admin,
|
||||
.bknd-admin {
|
||||
--color-primary: rgb(9 9 11); /* zinc-950 */
|
||||
--color-background: rgb(250 250 250); /* zinc-50 */
|
||||
--color-muted: rgb(228 228 231); /* ? */
|
||||
--color-darkest: rgb(0 0 0); /* black */
|
||||
--color-lightest: rgb(255 255 255); /* white */
|
||||
--color-primary: var(--color-zinc-950);
|
||||
--color-background: var(--color-zinc-50);
|
||||
--color-muted: var(--color-zinc-200);
|
||||
--color-darkest: var(--color-black);
|
||||
--color-lightest: var(--color-white);
|
||||
|
||||
--color-warning: var(--color-amber-100);
|
||||
--color-warning-foreground: var(--color-amber-800);
|
||||
--color-error: var(--color-red-100);
|
||||
--color-error-foreground: var(--color-red-800);
|
||||
--color-success: var(--color-green-100);
|
||||
--color-success-foreground: var(--color-green-800);
|
||||
--color-info: var(--color-blue-100);
|
||||
--color-info-foreground: var(--color-blue-800);
|
||||
|
||||
--color-resize: var(--color-blue-300);
|
||||
|
||||
@mixin light {
|
||||
--mantine-color-body: rgb(250 250 250);
|
||||
--mantine-color-body: var(--color-zinc-50);
|
||||
}
|
||||
@mixin dark {
|
||||
--mantine-color-body: rgb(9 9 11);
|
||||
--mantine-color-body: var(--color-zinc-950);
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -26,11 +37,20 @@
|
||||
.dark .bknd-admin /* currently used for elements, drop after making headless */,
|
||||
#bknd-admin.dark,
|
||||
.bknd-admin.dark {
|
||||
--color-primary: rgb(250 250 250); /* zinc-50 */
|
||||
--color-primary: var(--color-zinc-50);
|
||||
--color-background: rgb(30 31 34);
|
||||
--color-muted: rgb(47 47 52);
|
||||
--color-darkest: rgb(255 255 255); /* white */
|
||||
--color-lightest: rgb(24 24 27); /* black */
|
||||
--color-darkest: var(--color-white);
|
||||
--color-lightest: rgb(24 24 27);
|
||||
|
||||
--color-warning: var(--color-yellow-900);
|
||||
--color-warning-foreground: var(--color-yellow-200);
|
||||
--color-error: var(--color-red-950);
|
||||
--color-error-foreground: var(--color-red-200);
|
||||
--color-success: var(--color-green-950);
|
||||
--color-success-foreground: var(--color-green-200);
|
||||
--color-info: var(--color-blue-950);
|
||||
--color-info-foreground: var(--color-blue-200);
|
||||
}
|
||||
|
||||
@theme {
|
||||
@@ -39,6 +59,15 @@
|
||||
--color-muted: var(--color-muted);
|
||||
--color-darkest: var(--color-darkest);
|
||||
--color-lightest: var(--color-lightest);
|
||||
|
||||
--color-warning: var(--color-warning);
|
||||
--color-warning-foreground: var(--color-warning-foreground);
|
||||
--color-error: var(--color-error);
|
||||
--color-error-foreground: var(--color-error-foreground);
|
||||
--color-success: var(--color-success);
|
||||
--color-success-foreground: var(--color-success-foreground);
|
||||
--color-info: var(--color-info);
|
||||
--color-info-foreground: var(--color-info-foreground);
|
||||
}
|
||||
|
||||
#bknd-admin {
|
||||
|
||||
@@ -85,8 +85,6 @@ export function DataSchemaCanvas() {
|
||||
},
|
||||
}));
|
||||
|
||||
console.log("-", data, { nodes, edges });
|
||||
|
||||
const nodeLayout = layoutWithDagre({
|
||||
nodes: nodes.map((n) => ({
|
||||
id: n.id,
|
||||
|
||||
@@ -129,6 +129,7 @@ function AuthSettingsInternal() {
|
||||
name="jwt.secret"
|
||||
description="The secret used to sign the JWT token. If not set, a random key will be generated after enabling authentication."
|
||||
advanced="jwt"
|
||||
inputProps={{ type: "password" }}
|
||||
/>
|
||||
<AuthField name="jwt.alg" advanced="jwt" />
|
||||
<AuthField name="jwt.expires" advanced="jwt" />
|
||||
|
||||
23
app/src/ui/store/appshell.ts
Normal file
23
app/src/ui/store/appshell.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { combine, persist } from "zustand/middleware";
|
||||
|
||||
export const appShellStore = create(
|
||||
persist(
|
||||
combine(
|
||||
{
|
||||
sidebarOpen: false as boolean,
|
||||
sidebarWidth: 350 as number,
|
||||
},
|
||||
(set) => ({
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
closeSidebar: () => set({ sidebarOpen: false }),
|
||||
openSidebar: () => set({ sidebarOpen: true }),
|
||||
setSidebarWidth: (width: number) => set({ sidebarWidth: width }),
|
||||
resetSidebarWidth: () => set({ sidebarWidth: 350 }),
|
||||
}),
|
||||
),
|
||||
{
|
||||
name: "appshell",
|
||||
},
|
||||
),
|
||||
);
|
||||
1
app/src/ui/store/index.ts
Normal file
1
app/src/ui/store/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { appShellStore } from "./appshell";
|
||||
0
app/src/ui/store/utils.ts
Normal file
0
app/src/ui/store/utils.ts
Normal file
@@ -1,14 +1,15 @@
|
||||
import { createContext, lazy, useEffect, useState, Suspense, Fragment } from "react";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { App } from "bknd";
|
||||
import { checksum, secureRandomString } from "bknd/utils";
|
||||
import { checksum } from "bknd/utils";
|
||||
import { boolean, em, entity, text } from "bknd/data";
|
||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import IndexPage from "~/routes/_index";
|
||||
const Admin = lazy(() => import("~/routes/admin"));
|
||||
import { Center } from "~/components/Center";
|
||||
import { ClientProvider } from "bknd/client";
|
||||
|
||||
const Admin = lazy(() => import("~/routes/admin"));
|
||||
|
||||
export default function () {
|
||||
const [app, setApp] = useState<App | undefined>(undefined);
|
||||
const [hash, setHash] = useState<string>("");
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Center } from "~/components/Center";
|
||||
import type { App } from "bknd";
|
||||
import { useEntityQuery } from "bknd/client";
|
||||
import type { SQLocalConnection } from "@bknd/sqlocal/src";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function IndexPage({ app }: { app: App }) {
|
||||
const user = app.getApi().getUser();
|
||||
//const user = app.getApi().getUser();
|
||||
const limit = 5;
|
||||
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
|
||||
limit,
|
||||
sort: "-id",
|
||||
});
|
||||
// @ts-ignore
|
||||
const total = todos?.body.meta.total || 0;
|
||||
@@ -29,7 +32,8 @@ export default function IndexPage({ app }: { app: App }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-3">
|
||||
{todos?.reverse().map((todo) => (
|
||||
{todos &&
|
||||
[...todos].reverse().map((todo) => (
|
||||
<div className="flex flex-row" key={String(todo.id)}>
|
||||
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
|
||||
<input
|
||||
@@ -87,6 +91,49 @@ export default function IndexPage({ app }: { app: App }) {
|
||||
)}
|
||||
</div>*/}
|
||||
</div>
|
||||
<Debug app={app} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function Debug({ app }: { app: App }) {
|
||||
const [info, setInfo] = useState<any>();
|
||||
const connection = app.em.connection as SQLocalConnection;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setInfo(await connection.client.getDatabaseInfo());
|
||||
app.emgr.onAny(
|
||||
async () => {
|
||||
setInfo(await connection.client.getDatabaseInfo());
|
||||
},
|
||||
{ mode: "sync", id: "debug" },
|
||||
);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function download() {
|
||||
const databaseFile = await connection.client.getDatabaseFile();
|
||||
const fileUrl = URL.createObjectURL(databaseFile);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = fileUrl;
|
||||
a.download = "database.sqlite3";
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<button
|
||||
className="bg-foreground/20 leading-none py-2 px-3.5 rounded-lg text-sm hover:bg-foreground/30 transition-colors cursor-pointer"
|
||||
onClick={download}
|
||||
>
|
||||
Download Database
|
||||
</button>
|
||||
<pre className="text-xs">{JSON.stringify(info, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user