Save and persist desk settings independently per workspace
This commit is contained in:
1 parent
842c4754a9
commit
760ba2ff53
1 file changed
+191
-124
+191
-124
@@ -76,7 +76,7 @@ export const App: React.FC = () => {
|
||||
}
|
||||
|
||||
const currentBoard = targetBoard || currentSettings.deskTexture || 'oak';
|
||||
const updatedBoards = targetBoards || {
|
||||
const updatedBoards: BoardsMap = targetBoards || {
|
||||
...boardsRef.current,
|
||||
[currentBoard]: {
|
||||
cards: currentCards,
|
||||
@@ -85,6 +85,14 @@ export const App: React.FC = () => {
|
||||
settings: currentSettings
|
||||
}
|
||||
};
|
||||
if (!targetBoards) {
|
||||
updatedBoards[currentBoard] = {
|
||||
cards: currentCards,
|
||||
props: currentProps,
|
||||
strokes: currentStrokes,
|
||||
settings: currentSettings
|
||||
};
|
||||
}
|
||||
boardsRef.current = updatedBoards;
|
||||
|
||||
const doSave = () => {
|
||||
@@ -98,13 +106,7 @@ export const App: React.FC = () => {
|
||||
strokes: currentStrokes,
|
||||
activeBoard: currentBoard,
|
||||
boards: updatedBoards,
|
||||
settings: {
|
||||
deskTexture: currentBoard,
|
||||
lightAngle: currentSettings.lightAngle,
|
||||
grain: currentSettings.grain,
|
||||
layoutMode: currentSettings.layoutMode,
|
||||
showEvidenceStrings: currentSettings.showEvidenceStrings
|
||||
},
|
||||
settings: currentSettings,
|
||||
originClientId: clientIdRef.current
|
||||
})
|
||||
})
|
||||
@@ -161,13 +163,23 @@ export const App: React.FC = () => {
|
||||
if (currentBoardData.props) setPropsList(currentBoardData.props);
|
||||
if (currentBoardData.strokes) setStrokes(currentBoardData.strokes);
|
||||
|
||||
const boardSettings = currentBoardData.settings || desk.settings || {};
|
||||
const isMobile = window.innerWidth < 768;
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
...boardSettings,
|
||||
deskTexture: activeBoard,
|
||||
lightAngle: desk.settings?.lightAngle ?? s.lightAngle,
|
||||
grain: desk.settings?.grain ?? s.grain,
|
||||
layoutMode: desk.settings?.layoutMode ?? s.layoutMode,
|
||||
showEvidenceStrings: activeBoard === 'evidence-board' ? true : (desk.settings?.showEvidenceStrings ?? false)
|
||||
zoom: boardSettings.zoom ?? (isMobile ? 0.65 : 1.0),
|
||||
layoutMode: boardSettings.layoutMode ?? 'scatter',
|
||||
isLocked: boardSettings.isLocked ?? false,
|
||||
fontMode: boardSettings.fontMode ?? (activeBoard === 'blueprint' ? 'typed' : 'mixed'),
|
||||
lightAngle: boardSettings.lightAngle ?? (activeBoard === 'blueprint' ? 270 : 225),
|
||||
grain: boardSettings.grain ?? s.grain,
|
||||
showEvidenceStrings: boardSettings.showEvidenceStrings ?? (activeBoard === 'evidence-board'),
|
||||
showProps: boardSettings.showProps ?? true,
|
||||
cardCount: boardSettings.cardCount ?? (currentBoardData.cards ? currentBoardData.cards.length : 22),
|
||||
activeFilter: boardSettings.activeFilter ?? 'all',
|
||||
soundEnabled: desk.settings?.soundEnabled ?? s.soundEnabled
|
||||
}));
|
||||
|
||||
const maxZ = Math.max(15, ...(currentBoardData.cards || []).map((c: CardItem) => c.zIndex || 1));
|
||||
@@ -190,7 +202,7 @@ export const App: React.FC = () => {
|
||||
);
|
||||
setCards(initialCards);
|
||||
const initialBoards: BoardsMap = {
|
||||
oak: { cards: initialCards, props: INITIAL_PROPS, strokes: [] }
|
||||
oak: { cards: initialCards, props: INITIAL_PROPS, strokes: [], settings }
|
||||
};
|
||||
boardsRef.current = initialBoards;
|
||||
setMaxZIndex(initialCards.length + 20);
|
||||
@@ -232,14 +244,23 @@ export const App: React.FC = () => {
|
||||
}
|
||||
if (Array.isArray(payload.desk.props)) setPropsList(payload.desk.props);
|
||||
if (Array.isArray(payload.desk.strokes)) setStrokes(payload.desk.strokes);
|
||||
if (payload.desk.settings) {
|
||||
const boardSettings = (payload.desk.boards && payload.desk.boards[activeBoard]?.settings) || payload.desk.settings;
|
||||
if (boardSettings) {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
...boardSettings,
|
||||
deskTexture: activeBoard,
|
||||
lightAngle: payload.desk.settings.lightAngle ?? prev.lightAngle,
|
||||
grain: payload.desk.settings.grain ?? prev.grain,
|
||||
layoutMode: payload.desk.settings.layoutMode ?? prev.layoutMode,
|
||||
showEvidenceStrings: payload.desk.settings.showEvidenceStrings ?? prev.showEvidenceStrings
|
||||
zoom: boardSettings.zoom ?? prev.zoom,
|
||||
layoutMode: boardSettings.layoutMode ?? prev.layoutMode,
|
||||
isLocked: boardSettings.isLocked ?? prev.isLocked,
|
||||
fontMode: boardSettings.fontMode ?? prev.fontMode,
|
||||
lightAngle: boardSettings.lightAngle ?? prev.lightAngle,
|
||||
grain: boardSettings.grain ?? prev.grain,
|
||||
showEvidenceStrings: boardSettings.showEvidenceStrings ?? prev.showEvidenceStrings,
|
||||
showProps: boardSettings.showProps ?? prev.showProps,
|
||||
cardCount: boardSettings.cardCount ?? prev.cardCount,
|
||||
activeFilter: boardSettings.activeFilter ?? prev.activeFilter,
|
||||
soundEnabled: prev.soundEnabled
|
||||
}));
|
||||
}
|
||||
setSyncStatus('synced');
|
||||
@@ -262,68 +283,101 @@ export const App: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
// Update desk settings or switch unique workspace board
|
||||
const updateSettings = (newSettings: Partial<DeskSettings>) => {
|
||||
setSettings((prev) => {
|
||||
const updated = { ...prev, ...newSettings };
|
||||
const updateSettings = useCallback(
|
||||
(newSettingsInput: Partial<DeskSettings> | ((prev: DeskSettings) => Partial<DeskSettings>)) => {
|
||||
setSettings((prev) => {
|
||||
const newSettings = typeof newSettingsInput === 'function' ? newSettingsInput(prev) : newSettingsInput;
|
||||
const updated = { ...prev, ...newSettings };
|
||||
|
||||
// 1. Switch between 8 unique workspace boards
|
||||
if (newSettings.deskTexture !== undefined && newSettings.deskTexture !== prev.deskTexture) {
|
||||
const prevDesk = prev.deskTexture;
|
||||
const nextDesk = newSettings.deskTexture;
|
||||
// 1. Switch between 8 unique workspace boards
|
||||
if (newSettings.deskTexture !== undefined && newSettings.deskTexture !== prev.deskTexture) {
|
||||
const prevDesk = prev.deskTexture;
|
||||
const nextDesk = newSettings.deskTexture;
|
||||
|
||||
// Snapshot current board state
|
||||
const updatedBoards = {
|
||||
...boardsRef.current,
|
||||
[prevDesk]: {
|
||||
cards,
|
||||
props: propsList,
|
||||
strokes,
|
||||
settings: prev
|
||||
// Snapshot current board state with its active settings
|
||||
const updatedBoards: BoardsMap = {
|
||||
...boardsRef.current,
|
||||
[prevDesk]: {
|
||||
cards,
|
||||
props: propsList,
|
||||
strokes,
|
||||
settings: prev
|
||||
}
|
||||
};
|
||||
|
||||
// Load or initialize target board state
|
||||
let nextBoardData = updatedBoards[nextDesk];
|
||||
if (!nextBoardData || !Array.isArray(nextBoardData.cards) || nextBoardData.cards.length === 0) {
|
||||
if (nextDesk === 'oak') {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const seed = generateProceduralCards(w < 768 ? 9 : 20, w, h, prev.attachChance, prev.fontMode);
|
||||
nextBoardData = {
|
||||
cards: seed,
|
||||
props: INITIAL_PROPS,
|
||||
strokes: [],
|
||||
settings: { deskTexture: 'oak', zoom: w < 768 ? 0.65 : 1.0, lightAngle: 225, grain: 0.7 }
|
||||
};
|
||||
} else {
|
||||
nextBoardData = getDefaultBoardData(nextDesk, window.innerWidth, window.innerHeight);
|
||||
}
|
||||
updatedBoards[nextDesk] = nextBoardData;
|
||||
}
|
||||
};
|
||||
|
||||
// Load or initialize target board state
|
||||
let nextBoardData = updatedBoards[nextDesk];
|
||||
if (!nextBoardData || !Array.isArray(nextBoardData.cards) || nextBoardData.cards.length === 0) {
|
||||
if (nextDesk === 'oak') {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const seed = generateProceduralCards(w < 768 ? 9 : 20, w, h, updated.attachChance, updated.fontMode);
|
||||
nextBoardData = { cards: seed, props: INITIAL_PROPS, strokes: [] };
|
||||
} else {
|
||||
nextBoardData = getDefaultBoardData(nextDesk, window.innerWidth, window.innerHeight);
|
||||
}
|
||||
updatedBoards[nextDesk] = nextBoardData;
|
||||
// Restore target workspace's own saved settings
|
||||
const targetSavedSettings = nextBoardData.settings || {};
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
|
||||
|
||||
const nextSettings: DeskSettings = {
|
||||
...prev,
|
||||
...targetSavedSettings,
|
||||
...newSettings,
|
||||
deskTexture: nextDesk,
|
||||
zoom: newSettings.zoom ?? targetSavedSettings.zoom ?? (isMobile ? 0.65 : 1.0),
|
||||
layoutMode: newSettings.layoutMode ?? targetSavedSettings.layoutMode ?? 'scatter',
|
||||
isLocked: newSettings.isLocked ?? targetSavedSettings.isLocked ?? false,
|
||||
fontMode: newSettings.fontMode ?? targetSavedSettings.fontMode ?? (nextDesk === 'blueprint' ? 'typed' : 'mixed'),
|
||||
lightAngle: newSettings.lightAngle ?? targetSavedSettings.lightAngle ?? (nextDesk === 'blueprint' ? 270 : 225),
|
||||
grain: newSettings.grain ?? targetSavedSettings.grain ?? prev.grain,
|
||||
showEvidenceStrings: newSettings.showEvidenceStrings ?? targetSavedSettings.showEvidenceStrings ?? (nextDesk === 'evidence-board'),
|
||||
showProps: newSettings.showProps ?? targetSavedSettings.showProps ?? true,
|
||||
cardCount: newSettings.cardCount ?? targetSavedSettings.cardCount ?? (nextBoardData.cards ? nextBoardData.cards.length : prev.cardCount),
|
||||
activeFilter: newSettings.activeFilter ?? targetSavedSettings.activeFilter ?? 'all',
|
||||
// Preserve user global audio preference:
|
||||
soundEnabled: prev.soundEnabled
|
||||
};
|
||||
|
||||
updatedBoards[nextDesk] = {
|
||||
cards: nextBoardData.cards || [],
|
||||
props: nextBoardData.props || INITIAL_PROPS,
|
||||
strokes: nextBoardData.strokes || [],
|
||||
settings: nextSettings
|
||||
};
|
||||
|
||||
boardsRef.current = updatedBoards;
|
||||
setCards(nextBoardData.cards || []);
|
||||
setPropsList(nextBoardData.props || INITIAL_PROPS);
|
||||
setStrokes(nextBoardData.strokes || []);
|
||||
|
||||
saveToServer(
|
||||
nextBoardData.cards || [],
|
||||
nextBoardData.props || INITIAL_PROPS,
|
||||
nextBoardData.strokes || [],
|
||||
nextSettings,
|
||||
true,
|
||||
nextDesk,
|
||||
updatedBoards
|
||||
);
|
||||
|
||||
soundEngine.rustle(nextSettings.soundEnabled);
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
boardsRef.current = updatedBoards;
|
||||
setCards(nextBoardData.cards || []);
|
||||
setPropsList(nextBoardData.props || INITIAL_PROPS);
|
||||
setStrokes(nextBoardData.strokes || []);
|
||||
|
||||
const nextEvidenceStrings = nextDesk === 'evidence-board' ? true : (newSettings.showEvidenceStrings ?? false);
|
||||
|
||||
updated.deskTexture = nextDesk;
|
||||
updated.showEvidenceStrings = nextEvidenceStrings;
|
||||
|
||||
saveToServer(
|
||||
nextBoardData.cards || [],
|
||||
nextBoardData.props || INITIAL_PROPS,
|
||||
nextBoardData.strokes || [],
|
||||
updated,
|
||||
true,
|
||||
nextDesk,
|
||||
updatedBoards
|
||||
);
|
||||
|
||||
soundEngine.rustle(updated.soundEnabled);
|
||||
}
|
||||
|
||||
// 2. Real-time typography mode update across desk cards
|
||||
if (newSettings.fontMode !== undefined && newSettings.fontMode !== prev.fontMode) {
|
||||
const mode = newSettings.fontMode;
|
||||
setCards((prevCards) => {
|
||||
const next = prevCards.map((c) => {
|
||||
// 2. Real-time typography mode update across desk cards
|
||||
let nextCards = cards;
|
||||
if (newSettings.fontMode !== undefined && newSettings.fontMode !== prev.fontMode) {
|
||||
const mode = newSettings.fontMode;
|
||||
nextCards = cards.map((c) => {
|
||||
const fonts = mode === 'handwritten'
|
||||
? ['font-caveat', 'font-reenie', 'font-indie', 'font-shadows']
|
||||
: mode === 'typed'
|
||||
@@ -332,40 +386,55 @@ export const App: React.FC = () => {
|
||||
const chosen = fonts[Math.floor(Math.random() * fonts.length)];
|
||||
return { ...c, fontFamily: chosen };
|
||||
});
|
||||
saveToServer(next, propsList, strokes, updated, false);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Card density adjustment
|
||||
if (newSettings.cardCount !== undefined && newSettings.cardCount !== prev.cardCount) {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const diff = newSettings.cardCount - cards.length;
|
||||
if (diff > 0) {
|
||||
const added = generateProceduralCards(diff, w, h, updated.attachChance, updated.fontMode);
|
||||
setCards((c) => [...c, ...added]);
|
||||
} else if (diff < 0) {
|
||||
setCards((c) => c.slice(0, newSettings.cardCount));
|
||||
setCards(nextCards);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Attach chance adjustment
|
||||
if (newSettings.attachChance !== undefined && newSettings.attachChance !== prev.attachChance) {
|
||||
setCards((c) =>
|
||||
c.map((card) => {
|
||||
// 3. Card density adjustment
|
||||
if (newSettings.cardCount !== undefined && newSettings.cardCount !== prev.cardCount) {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
const diff = newSettings.cardCount - nextCards.length;
|
||||
if (diff > 0) {
|
||||
const added = generateProceduralCards(diff, w, h, updated.attachChance, updated.fontMode);
|
||||
nextCards = [...nextCards, ...added];
|
||||
setCards(nextCards);
|
||||
} else if (diff < 0) {
|
||||
nextCards = nextCards.slice(0, newSettings.cardCount);
|
||||
setCards(nextCards);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Attach chance adjustment
|
||||
if (newSettings.attachChance !== undefined && newSettings.attachChance !== prev.attachChance) {
|
||||
nextCards = nextCards.map((card) => {
|
||||
if (Math.random() < (newSettings.attachChance ?? 0.55)) {
|
||||
const types: CardItem['attachType'][] = ['washi', 'pin', 'paperclip'];
|
||||
return { ...card, attachType: types[Math.floor(Math.random() * types.length)] };
|
||||
}
|
||||
return { ...card, attachType: 'none' };
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
setCards(nextCards);
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
// 5. Snapshot and persist current workspace's updated settings
|
||||
const currentDesk = prev.deskTexture;
|
||||
const updatedBoards: BoardsMap = {
|
||||
...boardsRef.current,
|
||||
[currentDesk]: {
|
||||
cards: nextCards,
|
||||
props: propsList,
|
||||
strokes,
|
||||
settings: updated
|
||||
}
|
||||
};
|
||||
boardsRef.current = updatedBoards;
|
||||
|
||||
saveToServer(nextCards, propsList, strokes, updated, false, currentDesk, updatedBoards);
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[cards, propsList, strokes, saveToServer]
|
||||
);
|
||||
|
||||
// Re-shuffle cards (stratified positioning so cards never clump or run over each other)
|
||||
const handleShuffle = useCallback(() => {
|
||||
@@ -453,14 +522,17 @@ export const App: React.FC = () => {
|
||||
});
|
||||
}, [settings.zoom, settings.soundEnabled, saveToServer, propsList, strokes, settings]);
|
||||
|
||||
// Trigger re-layout when layoutMode or zoom changes
|
||||
// Trigger re-layout when layoutMode changes to grid
|
||||
const prevLayoutModeRef = useRef<string>(settings.layoutMode);
|
||||
useEffect(() => {
|
||||
if (settings.layoutMode === 'grid') {
|
||||
arrangeGrid();
|
||||
} else {
|
||||
handleShuffle();
|
||||
if (!isLoadedRef.current) return;
|
||||
if (prevLayoutModeRef.current !== settings.layoutMode) {
|
||||
prevLayoutModeRef.current = settings.layoutMode;
|
||||
if (settings.layoutMode === 'grid') {
|
||||
arrangeGrid();
|
||||
}
|
||||
}
|
||||
}, [settings.layoutMode, settings.zoom]);
|
||||
}, [settings.layoutMode, arrangeGrid]);
|
||||
|
||||
// Physics animation loop on requestAnimationFrame
|
||||
useEffect(() => {
|
||||
@@ -672,16 +744,15 @@ export const App: React.FC = () => {
|
||||
handleShuffle();
|
||||
} else if (e.key === 'g' || e.key === 'G') {
|
||||
e.preventDefault();
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
updateSettings((prev) => ({
|
||||
layoutMode: prev.layoutMode === 'scatter' ? 'grid' : 'scatter'
|
||||
}));
|
||||
} else if (e.key === 'p' || e.key === 'P') {
|
||||
e.preventDefault();
|
||||
setSettings((prev) => ({ ...prev, penMode: !prev.penMode }));
|
||||
updateSettings((prev) => ({ penMode: !prev.penMode }));
|
||||
} else if (e.key === 'l' || e.key === 'L') {
|
||||
e.preventDefault();
|
||||
setSettings((prev) => ({ ...prev, isLocked: !prev.isLocked }));
|
||||
updateSettings((prev) => ({ isLocked: !prev.isLocked }));
|
||||
} else if (e.key === 'n' || e.key === 'N') {
|
||||
e.preventDefault();
|
||||
setIsAddOpen(true);
|
||||
@@ -693,7 +764,7 @@ export const App: React.FC = () => {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleShuffle]);
|
||||
}, [handleShuffle, updateSettings]);
|
||||
|
||||
// Load Presets
|
||||
const handleLoadPreset = (presetName: string) => {
|
||||
@@ -703,7 +774,7 @@ export const App: React.FC = () => {
|
||||
if (presetName === 'original') {
|
||||
const generated = generateProceduralCards(20, w, h, 0.6, 'mixed');
|
||||
setCards(generated);
|
||||
setSettings((s) => ({ ...s, deskTexture: 'oak', lightAngle: 225, grain: 0.7 }));
|
||||
updateSettings({ deskTexture: 'oak', lightAngle: 225, grain: 0.7 });
|
||||
} else if (presetName === 'designer') {
|
||||
const designerCards: CardItem[] = SAMPLE_PRESETS.slice(0, 14).map((p, i) => ({
|
||||
id: `design-${i}`,
|
||||
@@ -725,7 +796,7 @@ export const App: React.FC = () => {
|
||||
todoItems: p.todoItems
|
||||
}));
|
||||
setCards(designerCards);
|
||||
setSettings((s) => ({ ...s, deskTexture: 'cutting-mat', lightAngle: 180 }));
|
||||
updateSettings({ deskTexture: 'cutting-mat', lightAngle: 180 });
|
||||
} else if (presetName === 'creative-coder') {
|
||||
const coderCards: CardItem[] = [
|
||||
{
|
||||
@@ -784,7 +855,7 @@ export const App: React.FC = () => {
|
||||
}
|
||||
];
|
||||
setCards(coderCards);
|
||||
setSettings((s) => ({ ...s, deskTexture: 'blueprint', lightAngle: 270 }));
|
||||
updateSettings({ deskTexture: 'blueprint', lightAngle: 270 });
|
||||
} else if (presetName === 'photojournal') {
|
||||
const photoCards = SAMPLE_PRESETS.filter((p) => p.type === 'polaroid').map((p, i) => ({
|
||||
id: `photo-${i}`,
|
||||
@@ -806,7 +877,7 @@ export const App: React.FC = () => {
|
||||
fontFamily: 'font-caveat'
|
||||
}));
|
||||
setCards(photoCards);
|
||||
setSettings((s) => ({ ...s, deskTexture: 'walnut', lightAngle: 225 }));
|
||||
updateSettings({ deskTexture: 'walnut', lightAngle: 225 });
|
||||
}
|
||||
soundEngine.shuffle(settings.soundEnabled);
|
||||
};
|
||||
@@ -963,20 +1034,19 @@ export const App: React.FC = () => {
|
||||
<BottomBar
|
||||
onShuffle={handleShuffle}
|
||||
onToggleLayout={() =>
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
updateSettings((prev) => ({
|
||||
layoutMode: prev.layoutMode === 'scatter' ? 'grid' : 'scatter'
|
||||
}))
|
||||
}
|
||||
onTogglePen={() => setSettings((prev) => ({ ...prev, penMode: !prev.penMode }))}
|
||||
onToggleLock={() => setSettings((prev) => ({ ...prev, isLocked: !prev.isLocked }))}
|
||||
onTogglePen={() => updateSettings((prev) => ({ penMode: !prev.penMode }))}
|
||||
onToggleLock={() => updateSettings((prev) => ({ isLocked: !prev.isLocked }))}
|
||||
onOpenSettings={() => setIsSettingsOpen(true)}
|
||||
onAddCard={() => setIsAddOpen(true)}
|
||||
onCycleZoom={() =>
|
||||
setSettings((prev) => {
|
||||
updateSettings((prev) => {
|
||||
const z = prev.zoom;
|
||||
const nextZoom = z <= 0.55 ? 0.75 : z <= 0.8 ? 1.0 : 0.5;
|
||||
return { ...prev, zoom: nextZoom };
|
||||
return { zoom: nextZoom };
|
||||
})
|
||||
}
|
||||
penActive={settings.penMode}
|
||||
@@ -1062,9 +1132,6 @@ export const App: React.FC = () => {
|
||||
settings={settings}
|
||||
onLoadPreset={(name) => {
|
||||
handleLoadPreset(name);
|
||||
setTimeout(() => {
|
||||
saveToServer(cards, propsList, strokes, settings, true);
|
||||
}, 100);
|
||||
}}
|
||||
onImportJson={(importedCards) => {
|
||||
setCards(importedCards);
|
||||
|
||||
Reference in new issue
Block a user