chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,196 @@
"use client";
import { useState } from "react";
import { FieldType } from "@planarchy/shared";
import type { BlueprintFieldDefinition } from "@planarchy/shared";
import { trpc } from "~/lib/trpc/client.js";
const INPUT_CLS =
"px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
interface Props {
selectedIds: string[];
fieldDefs: BlueprintFieldDefinition[];
onClose: () => void;
onSuccess: () => void;
}
export function BulkEditModal({ selectedIds, fieldDefs, onClose, onSuccess }: Props) {
// Track which fields are included in the bulk update + their new values
const [included, setIncluded] = useState<Set<string>>(new Set());
const [values, setValues] = useState<Record<string, unknown>>({});
const [error, setError] = useState<string | null>(null);
const utils = trpc.useUtils();
const mutation = trpc.resource.batchUpdateCustomFields.useMutation({
onSuccess: async () => {
await utils.resource.list.invalidate();
onSuccess();
onClose();
},
onError: (err) => setError(err.message),
});
function toggleInclude(key: string) {
setIncluded((prev) => {
const next = new Set(prev);
if (next.has(key)) { next.delete(key); } else { next.add(key); }
return next;
});
}
function setValue(key: string, value: unknown) {
setValues((prev) => ({ ...prev, [key]: value }));
}
function handleSave() {
setError(null);
const fields: Record<string, unknown> = {};
for (const key of included) {
fields[key] = values[key] ?? "";
}
if (Object.keys(fields).length === 0) {
setError("Select at least one field to update.");
return;
}
mutation.mutate({ ids: selectedIds, fields });
}
function handleBackdrop(e: React.MouseEvent<HTMLDivElement>) {
if (e.target === e.currentTarget) onClose();
}
const editableFields = fieldDefs.filter((f) => !f.required || included.has(f.key));
return (
<div
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
onClick={handleBackdrop}
>
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<div>
<h2 className="text-lg font-semibold text-gray-900">Bulk Edit Custom Fields</h2>
<p className="text-sm text-gray-500 mt-0.5">
Updating {selectedIds.length} resource{selectedIds.length !== 1 ? "s" : ""}
</p>
</div>
<button type="button" onClick={onClose} className="text-gray-400 hover:text-gray-600 text-2xl leading-none" aria-label="Close">×</button>
</div>
<div className="px-6 py-4 space-y-3 max-h-[60vh] overflow-y-auto">
{fieldDefs.length === 0 && (
<p className="text-sm text-gray-400 text-center py-6">No custom fields defined. Configure them in Admin Blueprints.</p>
)}
{fieldDefs.map((field) => (
<div key={field.key} className={`border rounded-lg p-3 transition-colors ${included.has(field.key) ? "border-brand-300 bg-brand-50" : "border-gray-200"}`}>
<label className="flex items-center gap-2 mb-2 cursor-pointer">
<input
type="checkbox"
checked={included.has(field.key)}
onChange={() => toggleInclude(field.key)}
className="rounded border-gray-300 text-brand-600"
/>
<span className="text-sm font-medium text-gray-700">{field.label}</span>
{field.required && <span className="text-xs text-red-500">required</span>}
</label>
{included.has(field.key) && (
<FieldInput
field={field}
value={values[field.key]}
onChange={(v) => setValue(field.key, v)}
/>
)}
</div>
))}
</div>
{error && (
<div className="mx-6 mb-2 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
)}
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200">
<p className="text-xs text-gray-400">{included.size} field{included.size !== 1 ? "s" : ""} selected</p>
<div className="flex gap-3">
<button type="button" onClick={onClose} className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium">
Cancel
</button>
<button
type="button"
onClick={handleSave}
disabled={mutation.isPending || included.size === 0}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{mutation.isPending ? "Saving…" : `Apply to ${selectedIds.length} resource${selectedIds.length !== 1 ? "s" : ""}`}
</button>
</div>
</div>
</div>
</div>
);
}
function FieldInput({ field, value, onChange }: { field: BlueprintFieldDefinition; value: unknown; onChange: (v: unknown) => void }) {
const str = value !== undefined && value !== null ? String(value) : "";
if (field.type === FieldType.BOOLEAN) {
return (
<select value={str} onChange={(e) => onChange(e.target.value === "true")} className={INPUT_CLS}>
<option value=""> select </option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
);
}
if (field.type === FieldType.SELECT && field.options) {
return (
<select value={str} onChange={(e) => onChange(e.target.value)} className={INPUT_CLS}>
<option value=""> select </option>
{field.options.map((o) => <option key={o.value} value={o.value}>{o.label || o.value}</option>)}
</select>
);
}
if (field.type === FieldType.NUMBER) {
return (
<input
type="number"
value={str}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : "")}
placeholder={field.placeholder}
className={INPUT_CLS}
/>
);
}
if (field.type === FieldType.DATE) {
return <input type="date" value={str} onChange={(e) => onChange(e.target.value)} className={INPUT_CLS} />;
}
if (field.type === FieldType.TEXTAREA) {
return (
<textarea
value={str}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className={`${INPUT_CLS} w-full resize-none`}
rows={3}
/>
);
}
return (
<input
type={field.type === FieldType.EMAIL ? "email" : field.type === FieldType.URL ? "url" : "text"}
value={str}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className={`${INPUT_CLS} w-full`}
/>
);
}
// Re-export for convenience
export type { Props as BulkEditModalProps };