feat: rubgram manual submission creation
Build and Push Image / docker (push) Successful in 5m49s

This commit is contained in:
2026-06-02 23:43:00 +07:00 Unverified
parent 809bb8d09c
commit 5d9b75aa9b
43 changed files with 1354 additions and 381 deletions
+192 -22
View File
@@ -2,6 +2,7 @@
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Button } from "@/components/ui/button";
import {
Command,
@@ -29,13 +30,45 @@ export function ComboBox({
defaultValue = "",
...props
}: React.ComponentProps<"button"> & {
data: { value: string; label: string }[];
data: { value: string; label: React.ReactNode; context?: string[] }[];
placeholder?: string;
trigger?: React.ReactNode;
onValueSelect?: (val: string) => void;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const [query, setQuery] = React.useState("");
const list = React.useMemo(
() =>
data.map((ent) => ({
context: (
ent.context?.join("") ??
"" + (typeof ent.label === "string" ? ent.label : "") + ent.value
).toLowerCase(),
comp: (
<CommandItem
key={ent.value}
value={ent.value}
onSelect={(currentValue) => {
if (onValueSelect) onValueSelect(currentValue);
else setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
{ent.label}
<Check
className={cn(
"ml-auto",
value === ent.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
),
})),
[data],
);
const queryLower = query.toLowerCase();
React.useEffect(() => {
if (defaultValue) setValue(defaultValue);
@@ -61,30 +94,18 @@ export function ComboBox({
)}
</PopoverTrigger>
<PopoverContent className="w-100 p-0">
<Command>
<CommandInput placeholder={placeholder} className="h-9" />
<Command shouldFilter={false}>
<CommandInput
onValueChange={setQuery}
placeholder={placeholder}
className="h-9"
/>
<CommandList>
<CommandEmpty>None found.</CommandEmpty>
<CommandGroup>
{data.map((ent) => (
<CommandItem
key={ent.value}
value={ent.value}
onSelect={(currentValue) => {
if (onValueSelect) onValueSelect(currentValue);
else setValue(currentValue === value ? "" : currentValue);
setOpen(false);
}}
>
{ent.label}
<Check
className={cn(
"ml-auto",
value === ent.value ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
{list
.filter((e) => e.context.includes(queryLower))
.map((e) => e.comp)}
</CommandGroup>
</CommandList>
</Command>
@@ -94,3 +115,152 @@ export function ComboBox({
</>
);
}
export function VirtualizedComboBox({
data,
placeholder = "Search...",
id,
name,
className,
trigger,
onValueSelect,
defaultValue = "",
itemHeight = 36,
...props
}: React.ComponentProps<"button"> & {
data: { value: string; label: React.ReactNode; context?: string[] }[];
placeholder?: string;
trigger?: React.ReactNode;
onValueSelect?: (val: string) => void;
itemHeight?: number;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(defaultValue);
const [query, setQuery] = React.useState("");
const filtered = React.useMemo(() => {
const q = query.toLowerCase();
if (!q) return data;
return data.filter((ent) => {
const ctx = (
ent.context?.join("") ??
"" + (typeof ent.label === "string" ? ent.label : "") + ent.value
).toLowerCase();
return ctx.includes(q);
});
}, [data, query]);
const listRef = React.useRef<HTMLDivElement>(null);
const [scrollEl, setScrollEl] = React.useState<HTMLDivElement | null>(null);
React.useLayoutEffect(() => {
setTimeout(() => {
setScrollEl(listRef.current);
}, 100);
}, [open, listRef]);
const virtualizer = useVirtualizer({
count: filtered.length,
getScrollElement: () => scrollEl,
estimateSize: () => itemHeight,
overscan: 5,
});
React.useEffect(() => {
if (defaultValue) setValue(defaultValue);
}, [defaultValue]);
const visibleItems = scrollEl ? virtualizer.getVirtualItems() : [];
return (
<>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
{trigger || (
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-50 justify-between", className)}
{...props}
>
<div>
{value
? data.find((ent) => ent.value === value)?.label
: placeholder}
</div>
<ChevronsUpDown className="opacity-50" />
</Button>
)}
</PopoverTrigger>
<PopoverContent className="w-100 p-0">
<Command shouldFilter={false}>
<CommandInput
onValueChange={setQuery}
placeholder={placeholder}
className="h-9"
/>
<CommandList>
<div
ref={listRef}
style={{ maxHeight: 300, overflowY: "auto" }}
onWheel={(e) => e.stopPropagation()}
>
{filtered.length === 0 ? (
<div className="py-6 text-center text-sm">None found.</div>
) : (
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{visibleItems.map((virtualItem) => {
const item = filtered[virtualItem.index];
return (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<CommandItem
value={item.value}
onSelect={(currentValue) => {
if (onValueSelect) onValueSelect(currentValue);
else
setValue(
currentValue === value ? "" : currentValue,
);
setOpen(false);
}}
>
{item.label}
<Check
className={cn(
"ml-auto",
value === item.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
</div>
);
})}
</div>
)}
</div>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<input id={id} name={name} type="hidden" value={value} />
</>
);
}
+28
View File
@@ -0,0 +1,28 @@
"use client";
import type { ErrorInfo } from "next/dist/client/components/error-boundary";
import { ModalBase } from "./modal";
import { Button } from "./ui/button";
import { DialogClose, DialogFooter } from "./ui/dialog";
export function ErrorModal({
error,
reset,
}: Omit<ErrorInfo, "unstable_retry">) {
return (
<ModalBase title="Modal Error">
An error occured while spawning modal:
<pre className="whitespace-pre-wrap break-all">
{error.stack?.split("\n").slice(0, 2).join("\n")}
</pre>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Close</Button>
</DialogClose>
<Button variant="destructive" onClick={reset}>
Retry
</Button>
</DialogFooter>
</ModalBase>
);
}
+15 -6
View File
@@ -9,6 +9,7 @@ import type {
import { cn } from "@/lib/utils";
import { DialogClose } from "./ui/dialog";
import { Label } from "./ui/label";
import { Divide } from "lucide-react";
const AUTOSAVE_TTL_MS = 10 * 60 * 1000;
const STORAGE_PREFIX = "form:autosave:";
@@ -28,7 +29,7 @@ type FormContextValue = {
const FormContext = React.createContext<FormContextValue | null>(null);
function useFormContext() {
export function useFormContext() {
const ctx = React.useContext(FormContext);
if (!ctx) throw new Error("FormInput/FormAction must be inside FormProvider");
return ctx;
@@ -153,9 +154,13 @@ export function FormProvider<T extends TypedFormDataShape>({
React.useEffect(() => {
const stored = safeParseAutosave(localStorage.getItem(storageKey(id)));
if (!stored) return;
if (!stored) {
setStorageLock(null);
return;
}
if (Date.now() - stored.savedAt > AUTOSAVE_TTL_MS) {
localStorage.removeItem(storageKey(id));
setStorageLock(null);
return;
}
setValues(stored.values || {});
@@ -282,8 +287,8 @@ export function FormInput({
className,
}: FormInputProps) {
const { values, setValue, updateValues } = useFormContext();
const childProps = children.props as Record<string, unknown>;
const value = (values[name] ?? childProps.defaultValue) || "";
const childProps = (children.props ?? {}) as Record<string, unknown>;
const value = values[name] ?? childProps?.defaultValue ?? "";
const nextProps: Record<string, unknown> = { name, defaultValue: undefined };
const applyValue = React.useCallback(
(nextValue: unknown) => {
@@ -336,6 +341,10 @@ export function FormInput({
nextProps.id = `forminput-${name}`;
}
if (children.$$typeof === Symbol.for("react.lazy")) {
children = React.use((children as any)._payload);
}
return (
<>
{label ? (
@@ -346,10 +355,10 @@ export function FormInput({
<span className="font-normal opacity-70">{subLabel}</span>
)}
</Label>
{React.cloneElement(children, nextProps)}
{React.cloneElement(children || Divide, nextProps)}
</div>
) : (
React.cloneElement(children, nextProps)
React.cloneElement(children || Divide, nextProps)
)}
</>
);