feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -0,0 +1,104 @@
import React from 'react';
function storageKey(slug: string) {
return `guestTasks_${slug}`;
}
function parseStored(value: string | null) {
if (!value) {
return [] as number[];
}
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.filter((item) => Number.isInteger(item)) as number[];
}
return [];
} catch (error) {
console.warn('Failed to parse task progress from storage', error);
return [];
}
}
export function useGuestTaskProgress(slug: string | undefined) {
const [completed, setCompleted] = React.useState<number[]>([]);
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
if (!slug) {
setCompleted([]);
setHydrated(true);
return;
}
try {
const stored = window.localStorage.getItem(storageKey(slug));
setCompleted(parseStored(stored));
} catch (error) {
console.warn('Failed to read task progress', error);
setCompleted([]);
} finally {
setHydrated(true);
}
}, [slug]);
const persist = React.useCallback(
(next: number[]) => {
if (!slug) return;
setCompleted(next);
try {
window.localStorage.setItem(storageKey(slug), JSON.stringify(next));
} catch (error) {
console.warn('Failed to persist task progress', error);
}
},
[slug]
);
const markCompleted = React.useCallback(
(taskId: number) => {
if (!slug || !Number.isInteger(taskId)) {
return;
}
setCompleted((prev) => {
if (prev.includes(taskId)) {
return prev;
}
const next = [...prev, taskId];
try {
window.localStorage.setItem(storageKey(slug), JSON.stringify(next));
} catch (error) {
console.warn('Failed to persist task progress', error);
}
return next;
});
},
[slug]
);
const clearProgress = React.useCallback(() => {
if (!slug) return;
setCompleted([]);
try {
window.localStorage.removeItem(storageKey(slug));
} catch (error) {
console.warn('Failed to clear task progress', error);
}
}, [slug]);
const isCompleted = React.useCallback(
(taskId: number | null | undefined) => {
if (!Number.isInteger(taskId)) return false;
return completed.includes(taskId as number);
},
[completed]
);
return {
hydrated,
completed,
completedCount: completed.length,
markCompleted,
clearProgress,
isCompleted,
};
}