fix(workshop): address review feedback and merge dev

This commit is contained in:
ajnart committed 2026-07-03 22:45:05 +02:00
commit b0429f9287
380 files changed
+12658 -13759

No files matched your search

+17 -10
View File
@@ -29,6 +29,9 @@ const sorters: Record<SortKey, (a: StoreSubmission, b: StoreSubmission) => numbe
new: (a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
};
const isNotFoundError = (error: unknown) =>
typeof error === "object" && error !== null && "status" in error && error.status === 404;
export const useStore = (storeUrl: string) => {
const pb = useMemo(() => getPocketBase(storeUrl), [storeUrl]);
const [submissions, setSubmissions] = useState<StoreSubmission[]>([]);
@@ -134,7 +137,6 @@ export const useStore = (storeUrl: string) => {
const isToggleOff = prev?.value === value;
const [upD, downD] = voteDelta(prev?.value, value);
// Optimistic update — instant UI feedback
setVotes((v) => {
const next = { ...v };
if (isToggleOff) delete next[submissionId];
@@ -151,27 +153,32 @@ export const useStore = (storeUrl: string) => {
),
);
// Server validates in background
try {
const existing = prev?.id ? prev : undefined;
if (!existing) {
await pb.collection("votes").create({ submission: submissionId, value, user: userId });
const created = await pb
.collection("votes")
.create<StoreVote>({ submission: submissionId, value, user: userId });
setVotes((current) => ({ ...current, [submissionId]: created }));
} else if (isToggleOff) {
try {
await pb.collection("votes").delete(existing.id);
} catch {
// Already deleted server-side — fine
} catch (caught) {
if (!isNotFoundError(caught)) throw caught;
}
} else {
try {
await pb.collection("votes").update(existing.id, { value });
} catch {
// Record gone server-side — recreate
await pb.collection("votes").create({ submission: submissionId, value, user: userId });
const updated = await pb.collection("votes").update<StoreVote>(existing.id, { value });
setVotes((current) => ({ ...current, [submissionId]: updated }));
} catch (caught) {
if (!isNotFoundError(caught)) throw caught;
const created = await pb
.collection("votes")
.create<StoreVote>({ submission: submissionId, value, user: userId });
setVotes((current) => ({ ...current, [submissionId]: created }));
}
}
} catch (caught) {
// Revert optimistic state
setVotes((v) => {
const next = { ...v };
if (prev) next[submissionId] = prev;
+1 -1
View File
@@ -12,7 +12,7 @@ export const getPocketBase = (url: string): PocketBase => {
};
export const getSubmissionFileUrl = (baseUrl: string, submissionId: string, filename: string) =>
`${baseUrl}/api/files/submissions/${submissionId}/${filename}`;
`${baseUrl}/api/files/submissions/${submissionId}/${encodeURIComponent(filename)}`;
export interface StoreSubmission {
id: string;
+13
View File
@@ -25,6 +25,19 @@ describe("validateSubmissionContent", () => {
expect(result.success).toBe(false);
});
it("rejects widgets without a schema version", () => {
const widgetWithoutSchema = {
name: validWidget.name,
url: validWidget.url,
authType: validWidget.authType,
method: validWidget.method,
displayType: validWidget.displayType,
displayConfig: validWidget.displayConfig,
};
expect(validateSubmissionContent("widget", JSON.stringify(widgetWithoutSchema)).success).toBe(false);
});
it("rejects malformed widget JSON", () => {
expect(validateSubmissionContent("widget", "{ not json").success).toBe(false);
});
+1 -1
View File
@@ -26,7 +26,7 @@ const displayTypes = [
const widgetSchema = z
.object({
$schema: z.literal(WIDGET_SCHEMA_VERSION).optional(),
$schema: z.literal(WIDGET_SCHEMA_VERSION),
name: z.string().min(1).max(128),
url: z.string().min(1),
authType: z.enum(authTypes),