Backend: - User.role (user/uploader/staff) with can_upload; uploads and downloads gated to uploader+; owners and staff can edit their items - MediaItem.uploaded_by plus J-<id> identity (serializer, admin, scan_files --user, first superuser as default owner) - API resolves J-<id>, bare numeric ids and MD5s; neighbors and lookup return j_ids - Guest safety: mirror e621's anonymous default blacklist into Redis (parses comments, negations and wildcards), flag hidden_from_guests and filter lists, details and lookups for anonymous users - POST /api/online/downloads/ writes an e621 file into the watched folder and indexes it for the uploader - MariaDB + Redis via docker compose (host ports 3307/6380), PyMySQL driver shim, Redis cache replacing the file cache; SQLite data dumped and loaded into MariaDB Frontend: - Single /detail/:itemId route with an adaptive shell: J-<id> renders the library item, bare numbers render the e621 post - Legacy /view/<md5> and /online/view/<id> redirect to canonical URLs - Cards expose J-IDs; library custom-data editor is read-only for non-owners - Role gating: Upload hidden/blocked for regular users, account shows the role, guest hint on the library
94 lines
2.7 KiB
TypeScript
94 lines
2.7 KiB
TypeScript
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
|
|
import { cn } from "@/lib/cn";
|
|
|
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
|
|
const buttonVariants: Record<ButtonVariant, string> = {
|
|
primary: "bg-ctp-mauve text-ctp-crust hover:brightness-110",
|
|
secondary:
|
|
"border border-ctp-surface1 bg-ctp-surface0 text-ctp-text hover:bg-ctp-surface1",
|
|
ghost: "text-ctp-subtext0 hover:bg-ctp-surface0 hover:text-ctp-text",
|
|
danger: "border border-ctp-red/40 text-ctp-red hover:bg-ctp-red/15",
|
|
};
|
|
|
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: ButtonVariant;
|
|
}
|
|
|
|
export function Button({ variant = "primary", className, ...props }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={cn(
|
|
"inline-flex items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition",
|
|
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ctp-mauve",
|
|
"active:scale-[.98] disabled:pointer-events-none disabled:opacity-50",
|
|
buttonVariants[variant],
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function Spinner({ className }: { className?: string }) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"h-4 w-4 animate-spin rounded-full border-2 border-ctp-surface2 border-t-ctp-mauve",
|
|
className,
|
|
)}
|
|
role="status"
|
|
aria-label="Loading"
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function FullPageSpinner() {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-ctp-crust">
|
|
<Spinner className="h-6 w-6" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Field({
|
|
label,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<label className="flex flex-col gap-1.5">
|
|
<span className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
|
|
{label}
|
|
</span>
|
|
{children}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export const inputClass =
|
|
"w-full rounded-md border border-ctp-surface0 bg-ctp-mantle px-3 py-2 text-sm text-ctp-text placeholder:text-ctp-overlay0 focus:border-ctp-mauve focus:outline-none";
|
|
|
|
export const linkButtonClass =
|
|
"inline-flex items-center justify-center gap-2 rounded-md border border-ctp-surface1 bg-ctp-surface0 px-3 py-1.5 text-sm font-medium text-ctp-text transition hover:bg-ctp-surface1";
|
|
|
|
export function EmptyState({
|
|
title,
|
|
description,
|
|
}: {
|
|
title: string;
|
|
description?: ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-ctp-surface1 px-6 py-16 text-center">
|
|
<p className="text-sm font-medium text-ctp-subtext1">{title}</p>
|
|
{description ? (
|
|
<p className="text-sm text-ctp-overlay0">{description}</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|