table and query work

This commit is contained in:
2026-03-27 18:30:50 -05:00
parent ddcb7e76a3
commit 32998d417f
41 changed files with 1886 additions and 42 deletions

View File

@@ -6,7 +6,7 @@ import { FieldErrors } from "./Errors.Field";
type InputFieldProps = {
label: string;
inputType: string;
required: boolean;
required?: boolean;
};
const autoCompleteMap: Record<string, string> = {
@@ -16,7 +16,11 @@ const autoCompleteMap: Record<string, string> = {
username: "username",
};
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
export const InputField = ({
label,
inputType,
required = false,
}: InputFieldProps) => {
const field = useFieldContext<any>();
return (

View File

@@ -1,13 +1,19 @@
import { useStore } from "@tanstack/react-form";
import { Button } from "@/components/ui/button";
import { useFormContext } from ".";
import { Spinner } from "@/components/ui/spinner";
import { useFormContext } from ".";
type SubmitButtonProps = {
children: React.ReactNode;
variant?: "default" | "secondary" | "destructive";
className?: string;
};
export const SubmitButton = ({ children }: SubmitButtonProps) => {
export const SubmitButton = ({
children,
variant = "default",
className,
}: SubmitButtonProps) => {
const form = useFormContext();
const [isSubmitting] = useStore(form.store, (state) => [
@@ -17,10 +23,19 @@ export const SubmitButton = ({ children }: SubmitButtonProps) => {
return (
<div className="">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? <><Spinner data-icon="inline-start" /> Submitting </> : <>{children}</>
}
<Button
type="submit"
disabled={isSubmitting}
variant={variant}
className={className}
>
{isSubmitting ? (
<>
<Spinner data-icon="inline-start" /> Submitting{" "}
</>
) : (
<>{children}</>
)}
</Button>
</div>
);

View File

@@ -0,0 +1,29 @@
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { useFieldContext } from ".";
type SwitchField = {
trueLabel: string;
falseLabel: string;
};
export const SwitchField = ({
trueLabel = "True",
falseLabel = "False",
}: SwitchField) => {
const field = useFieldContext<boolean>();
const checked = field.state.value ?? false;
return (
<div className="flex items-center space-x-2">
<Switch
id={field.name}
checked={checked}
onCheckedChange={field.handleChange}
onBlur={field.handleBlur}
/>
<Label htmlFor={field.name}>{checked ? trueLabel : falseLabel}</Label>
</div>
);
};

View File

@@ -3,6 +3,7 @@ import { CheckboxField } from "./CheckBox.Field";
import { InputField } from "./Input.Field";
import { InputPasswordField } from "./InputPassword.Field";
import { SubmitButton } from "./SubmitButton";
import { SwitchField } from "./Switch.Field";
export const { fieldContext, useFieldContext, formContext, useFormContext } =
createFormHookContexts();
@@ -16,6 +17,7 @@ export const { useAppForm } = createFormHook({
//DateField,
//TextArea,
//Searchable,
SwitchField,
},
formComponents: { SubmitButton },
fieldContext,

View File

@@ -0,0 +1,22 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import axios from "axios";
export function getSettings() {
return queryOptions({
queryKey: ["getSettings"],
queryFn: () => fetch(),
staleTime: 5000,
refetchOnWindowFocus: true,
placeholderData: keepPreviousData,
});
}
const fetch = async () => {
if (window.location.hostname === "localhost") {
await new Promise((res) => setTimeout(res, 5000));
}
const { data } = await axios.get("/lst/api/settings");
return data.data;
};

View File

@@ -0,0 +1,70 @@
import { useEffect, useRef, useState } from "react";
import { Input } from "../../components/ui/input";
type EditableCell = {
value: string | number | null | undefined;
id: string;
field: string;
className?: string;
onSubmit: (args: { id: string; field: string; value: string }) => void;
};
export default function EditableCellInput({
value,
id,
field,
className = "w-32",
onSubmit,
}: EditableCell) {
const initialValue = String(value ?? "");
const [localValue, setLocalValue] = useState(initialValue);
const submitting = useRef(false);
useEffect(() => {
setLocalValue(initialValue);
}, [initialValue]);
const handleSubmit = (nextValue: string) => {
const trimmedValue = nextValue.trim();
if (trimmedValue === initialValue) return;
onSubmit({
id,
field,
value: trimmedValue,
});
};
return (
<Input
value={localValue}
className={className}
onChange={(e) => setLocalValue(e.currentTarget.value)}
onBlur={(e) => {
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
setTimeout(() => {
submitting.current = false;
}, 100);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
e.preventDefault();
if (submitting.current) return;
submitting.current = true;
handleSubmit(e.currentTarget.value);
e.currentTarget.blur();
setTimeout(() => {
submitting.current = false;
}, 100);
}}
/>
);
}

View File

@@ -0,0 +1,129 @@
import {
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import { ScrollArea, ScrollBar } from "../../components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
import { cn } from "../utils";
type LstTableType = {
className?: string;
tableClassName?: string;
data: any;
columns: any;
};
export default function LstTable({
className = "",
tableClassName = "",
data,
columns,
}: LstTableType) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
//console.log(data);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
//getRowCanExpand: () => true,
filterFns: {},
state: {
sorting,
columnFilters,
},
});
return (
<div className={className}>
<ScrollArea className="w-full rounded-md border whitespace-nowrap">
<Table className={cn("w-full", tableClassName)}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import type { Column } from "@tanstack/react-table";
import { ArrowDown, ArrowUp, Search } from "lucide-react";
import React, { useState } from "react";
import { Button } from "../../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "../../components/ui/dropdown-menu";
import { Input } from "../../components/ui/input";
import { cn } from "../utils";
type SearchableHeaderProps<TData> = {
column: Column<TData, unknown>;
title: string;
searchable?: boolean;
};
export default function SearchableHeader<TData>({
column,
title,
searchable = false,
}: SearchableHeaderProps<TData>) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
className="px-2"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="flex flex-row items-center gap-2">
{title}
{column.getIsSorted() === "asc" ? (
<ArrowUp className="h-4 w-4" />
) : column.getIsSorted() === "desc" ? (
<ArrowDown className="h-4 w-4" />
) : null}
</span>
</Button>
{searchable && (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Search
className={cn(
"h-4 w-4",
column.getFilterValue() ? "text-primary" : "",
)}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 p-2">
<Input
autoFocus
value={(column.getFilterValue() as string) ?? ""}
onChange={(e) => column.setFilterValue(e.target.value)}
placeholder={`Search ${title.toLowerCase()}...`}
className="h-8"
/>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { Skeleton } from "../../components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table";
type TableSkelly = {
rows?: number;
columns?: number;
};
export default function SkellyTable({ rows = 5, columns = 4 }: TableSkelly) {
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-[80px]" />
</TableHead>
))}
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, r) => (
<TableRow key={r}>
{Array.from({ length: columns }).map((_, c) => (
<TableCell key={c}>
<Skeleton className="h-4 w-full max-w-[120px]" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}