feat(admin): server-safe StatusCard and Import hub polish
Split OnlineUsersWidget from StatusCard, decouple ad delete button, sync badge import to ExternalTexts+WebsiteBadges, add Import section hub with cancelable SSE jobs and upload SQL option. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
db957d7fb1
commit
9c4949186c
21 files changed
+576
-249
No files matched your search
@@ -9,11 +9,15 @@ import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
|
||||
const CLASSNAME_RE = /^[a-zA-Z0-9_]+$/;
|
||||
const NITRO_MAX_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
interface UploadResult {
|
||||
itemId: number;
|
||||
catalogItemId: number | null;
|
||||
classname: string;
|
||||
warnings: string[];
|
||||
sqlFile?: string | null;
|
||||
}
|
||||
|
||||
export function UploadFurniClient() {
|
||||
@@ -31,6 +35,7 @@ export function UploadFurniClient() {
|
||||
const [canlayon, setCanlayon] = useState(false);
|
||||
const [interactionType, setInteractionType] = useState("default");
|
||||
const [customparams, setCustomparams] = useState("");
|
||||
const [generateSql, setGenerateSql] = useState(false);
|
||||
|
||||
const [nitroFile, setNitroFile] = useState<File | null>(null);
|
||||
const [iconFile, setIconFile] = useState<File | null>(null);
|
||||
@@ -51,6 +56,7 @@ export function UploadFurniClient() {
|
||||
setCanlayon(false);
|
||||
setInteractionType("default");
|
||||
setCustomparams("");
|
||||
setGenerateSql(false);
|
||||
setNitroFile(null);
|
||||
setIconFile(null);
|
||||
setResult(null);
|
||||
@@ -61,13 +67,35 @@ export function UploadFurniClient() {
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setUploading(true);
|
||||
setError("");
|
||||
setResult(null);
|
||||
|
||||
const trimmedClass = classname.trim();
|
||||
if (!CLASSNAME_RE.test(trimmedClass)) {
|
||||
const msg =
|
||||
"Classname must contain only letters, numbers, and underscores";
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
if (!nitroFile) {
|
||||
const msg = "Nitro file is required";
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
if (nitroFile.size > NITRO_MAX_BYTES) {
|
||||
const msg = "Nitro file must be 50MB or smaller";
|
||||
setError(msg);
|
||||
toast.error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.set("classname", classname.trim());
|
||||
fd.set("classname", trimmedClass);
|
||||
fd.set("name", name.trim());
|
||||
fd.set("description", description.trim());
|
||||
fd.set("itemType", itemType);
|
||||
@@ -78,7 +106,8 @@ export function UploadFurniClient() {
|
||||
fd.set("canlayon", String(canlayon));
|
||||
fd.set("interactionType", interactionType.trim());
|
||||
fd.set("customparams", customparams.trim());
|
||||
if (nitroFile) fd.set("nitro", nitroFile);
|
||||
if (generateSql) fd.set("generateSql", "true");
|
||||
fd.set("nitro", nitroFile);
|
||||
if (iconFile) fd.set("icon", iconFile);
|
||||
|
||||
const res = await adminFetch("/api/admin/import/furni/upload", {
|
||||
@@ -219,6 +248,15 @@ export function UploadFurniClient() {
|
||||
/>
|
||||
<span className="text-sm">Can lay on</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={generateSql}
|
||||
onChange={(e) => setGenerateSql(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
<span className="text-sm">Generate SQL</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
@@ -304,6 +342,12 @@ export function UploadFurniClient() {
|
||||
<code className="font-mono">{result.catalogItemId}</code>
|
||||
</div>
|
||||
)}
|
||||
{result.sqlFile ? (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground w-32">SQL file:</span>
|
||||
<code className="font-mono break-all">{result.sqlFile}</code>
|
||||
</div>
|
||||
) : null}
|
||||
{result.warnings.length > 0 && (
|
||||
<div className="space-y-1 pt-2">
|
||||
<span className="text-muted-foreground font-medium">
|
||||
|
||||
Reference in new issue
Block a user