aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/components/EditDeckModal.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-02 11:46:13 +0900
committernsfisis <nsfisis@gmail.com>2026-05-02 11:46:13 +0900
commit023d0fcfce575030ee503c5f60df8c28dba7ab07 (patch)
tree2f8ab3915f338232619ec66bb272e4756a96e021 /src/client/components/EditDeckModal.tsx
parent13a3d16ffc88845d7bc65fb0778da9aaff53b653 (diff)
downloadkioku-023d0fcfce575030ee503c5f60df8c28dba7ab07.tar.gz
kioku-023d0fcfce575030ee503c5f60df8c28dba7ab07.tar.zst
kioku-023d0fcfce575030ee503c5f60df8c28dba7ab07.zip
feat(decks): make deck CRUD work fully offline-first
Create / Edit / Delete deck modals now write through localDeckRepository and fire-and-forget syncActionAtom so the change is pushed when the network is up. EditDeckModal reads its note-type list from the local-first noteTypesAtom instead of fetching, and the "reconnect to..." guards on the submit buttons are gone — the user can keep working while offline. Soft-delete intentionally does NOT cascade to notes/cards, matching the server's existing deck.softDelete: the deck disappears from listings and its children become unreachable that way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/client/components/EditDeckModal.tsx')
-rw-r--r--src/client/components/EditDeckModal.tsx111
1 files changed, 43 insertions, 68 deletions
diff --git a/src/client/components/EditDeckModal.tsx b/src/client/components/EditDeckModal.tsx
index e9c2b7b..dc7ec11 100644
--- a/src/client/components/EditDeckModal.tsx
+++ b/src/client/components/EditDeckModal.tsx
@@ -1,7 +1,7 @@
-import { useAtomValue } from "jotai";
-import { type FormEvent, useCallback, useEffect, useState } from "react";
-import { ApiClientError, apiClient } from "../api";
-import { isOnlineAtom } from "../atoms";
+import { useAtomValue, useSetAtom } from "jotai";
+import { type FormEvent, useEffect, useState } from "react";
+import { noteTypesAtom, syncActionAtom } from "../atoms";
+import { localDeckRepository } from "../db/repositories";
interface Deck {
id: string;
@@ -10,11 +10,6 @@ interface Deck {
defaultNoteTypeId: string | null;
}
-interface NoteTypeSummary {
- id: string;
- name: string;
-}
-
interface EditDeckModalProps {
isOpen: boolean;
deck: Deck | null;
@@ -22,54 +17,43 @@ interface EditDeckModalProps {
onDeckUpdated: () => void;
}
-export function EditDeckModal({
- isOpen,
+export function EditDeckModal(props: EditDeckModalProps) {
+ if (!props.isOpen || !props.deck) {
+ return null;
+ }
+ // Render the body only when actually open so the suspense-driven note types
+ // query does not fire on every host render (e.g. HomePage keeps the modal
+ // mounted at all times).
+ return <EditDeckModalContent {...props} deck={props.deck} />;
+}
+
+interface EditDeckModalContentProps extends EditDeckModalProps {
+ deck: Deck;
+}
+
+function EditDeckModalContent({
deck,
onClose,
onDeckUpdated,
-}: EditDeckModalProps) {
- const [name, setName] = useState("");
- const [description, setDescription] = useState("");
+}: EditDeckModalContentProps) {
+ const [name, setName] = useState(deck.name);
+ const [description, setDescription] = useState(deck.description ?? "");
const [defaultNoteTypeId, setDefaultNoteTypeId] = useState<string | null>(
- null,
+ deck.defaultNoteTypeId,
);
- const [noteTypes, setNoteTypes] = useState<NoteTypeSummary[]>([]);
- const [isLoadingNoteTypes, setIsLoadingNoteTypes] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
- const isOnline = useAtomValue(isOnlineAtom);
+ const noteTypesQuery = useAtomValue(noteTypesAtom);
+ const noteTypes = noteTypesQuery.data ?? [];
+ const triggerSync = useSetAtom(syncActionAtom);
- const fetchNoteTypes = useCallback(async () => {
- setIsLoadingNoteTypes(true);
- try {
- const res = await apiClient.rpc.api["note-types"].$get();
- const data = await apiClient.handleResponse<{
- noteTypes: NoteTypeSummary[];
- }>(res);
- setNoteTypes(data.noteTypes);
- } catch {
- // Non-critical: note type list is optional
- } finally {
- setIsLoadingNoteTypes(false);
- }
- }, []);
-
- // Sync form state when deck changes
useEffect(() => {
- if (deck) {
- setName(deck.name);
- setDescription(deck.description ?? "");
- setDefaultNoteTypeId(deck.defaultNoteTypeId);
- setError(null);
- }
+ setName(deck.name);
+ setDescription(deck.description ?? "");
+ setDefaultNoteTypeId(deck.defaultNoteTypeId);
+ setError(null);
}, [deck]);
- useEffect(() => {
- if (isOpen) {
- fetchNoteTypes();
- }
- }, [isOpen, fetchNoteTypes]);
-
const handleClose = () => {
setError(null);
onClose();
@@ -77,39 +61,31 @@ export function EditDeckModal({
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
- if (!deck) return;
setError(null);
setIsSubmitting(true);
try {
- const res = await apiClient.rpc.api.decks[":id"].$put({
- param: { id: deck.id },
- json: {
- name: name.trim(),
- description: description.trim() || null,
- defaultNoteTypeId: defaultNoteTypeId || null,
- },
+ const updated = await localDeckRepository.update(deck.id, {
+ name: name.trim(),
+ description: description.trim() || null,
+ defaultNoteTypeId: defaultNoteTypeId || null,
});
- await apiClient.handleResponse(res);
+ if (!updated) {
+ setError("Deck not found.");
+ return;
+ }
onDeckUpdated();
onClose();
- } catch (err) {
- if (err instanceof ApiClientError) {
- setError(err.message);
- } else {
- setError("Failed to update deck. Please try again.");
- }
+ void triggerSync().catch(() => {});
+ } catch {
+ setError("Failed to update deck. Please try again.");
} finally {
setIsSubmitting(false);
}
};
- if (!isOpen || !deck) {
- return null;
- }
-
return (
<div
role="dialog"
@@ -196,7 +172,7 @@ export function EditDeckModal({
id="edit-deck-default-note-type"
value={defaultNoteTypeId ?? ""}
onChange={(e) => setDefaultNoteTypeId(e.target.value || null)}
- disabled={isSubmitting || isLoadingNoteTypes}
+ disabled={isSubmitting}
className="w-full px-4 py-2.5 bg-ivory border border-border rounded-lg text-slate transition-all duration-200 hover:border-muted focus:border-primary focus:ring-2 focus:ring-primary/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
<option value="">None</option>
@@ -219,8 +195,7 @@ export function EditDeckModal({
</button>
<button
type="submit"
- disabled={isSubmitting || !name.trim() || !isOnline}
- title={!isOnline ? "Reconnect to save changes" : undefined}
+ disabled={isSubmitting || !name.trim()}
className="px-4 py-2 bg-primary hover:bg-primary-dark text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "Saving..." : "Save Changes"}