From 3a22b4249210209e963cc2bee15a88ea1b02412a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 13 Jan 2026 16:52:57 -0700 Subject: [PATCH] Phase 6 complete --- IMPLEMENTATION_PLAN.md | 42 ++-- components/Landing.tsx | 15 +- components/QuizLibrary.tsx | 58 ++++-- hooks/useAuthenticatedFetch.ts | 32 ++- hooks/useQuizLibrary.ts | 189 ++++++++++++++---- index.tsx | 26 +++ package-lock.json | 34 ++++ package.json | 17 +- server/src/index.ts | 15 +- server/src/routes/quizzes.ts | 64 +++++- server/tests/api.test.ts | 346 ++++++++++++++++++++++++++++++++- 11 files changed, 735 insertions(+), 103 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index cc3656e..2a80a42 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -267,32 +267,32 @@ Add user accounts via Authentik (OIDC) and persist quizzes to SQLite database. U ## Phase 6: Polish & Error Handling ### 6.1 Loading States -- [ ] Add loading indicators: - - [ ] Quiz library list loading - - [ ] Quiz loading when selected - - [ ] Save operation in progress -- [ ] Disable buttons during async operations +- [x] Add loading indicators: + - [x] Quiz library list loading + - [x] Quiz loading when selected + - [x] Save operation in progress +- [x] Disable buttons during async operations ### 6.2 Error Handling -- [ ] Display user-friendly error messages: - - [ ] Failed to load quiz library - - [ ] Failed to save quiz - - [ ] Failed to delete quiz - - [ ] Network/auth errors -- [ ] Add retry mechanisms where appropriate +- [x] Display user-friendly error messages: + - [x] Failed to load quiz library + - [x] Failed to save quiz + - [x] Failed to delete quiz + - [x] Network/auth errors +- [x] Add retry mechanisms where appropriate ### 6.3 Toast Notifications (Optional) -- [ ] Add `react-hot-toast` or similar -- [ ] Show success toasts: - - [ ] "Quiz saved successfully" - - [ ] "Quiz deleted" -- [ ] Show error toasts for failures +- [x] Add `react-hot-toast` or similar +- [x] Show success toasts: + - [x] "Quiz saved successfully" + - [x] "Quiz deleted" +- [x] Show error toasts for failures ### 6.4 Edge Cases -- [ ] Handle auth token expiry gracefully -- [ ] Handle offline state -- [ ] Handle concurrent save attempts -- [ ] Validate quiz data before save +- [x] Handle auth token expiry gracefully +- [x] Handle offline state +- [x] Handle concurrent save attempts +- [x] Validate quiz data before save --- @@ -408,5 +408,5 @@ kaboot/ | Phase 3 | **COMPLETE** | OIDC config, AuthProvider, AuthButton, useAuthenticatedFetch | | Phase 4 | **COMPLETE** | useQuizLibrary hook, QuizLibrary modal, Landing integration | | Phase 5 | **COMPLETE** | SaveQuizPrompt modal, QuizCreator save checkbox, save integration | -| Phase 6 | Not Started | | +| Phase 6 | **COMPLETE** | Toast notifications, loading states, error handling, edge cases | | Phase 7 | Not Started | | diff --git a/components/Landing.tsx b/components/Landing.tsx index 6b90774..8e69678 100644 --- a/components/Landing.tsx +++ b/components/Landing.tsx @@ -24,7 +24,17 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on const [name, setName] = useState(''); const [libraryOpen, setLibraryOpen] = useState(false); - const { quizzes, loading: libraryLoading, error: libraryError, fetchQuizzes, loadQuiz, deleteQuiz } = useQuizLibrary(); + const { + quizzes, + loading: libraryLoading, + loadingQuizId, + deletingQuizId, + error: libraryError, + fetchQuizzes, + loadQuiz, + deleteQuiz, + retry: retryLibrary + } = useQuizLibrary(); useEffect(() => { if (libraryOpen && auth.isAuthenticated) { @@ -169,9 +179,12 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on onClose={() => setLibraryOpen(false)} quizzes={quizzes} loading={libraryLoading} + loadingQuizId={loadingQuizId} + deletingQuizId={deletingQuizId} error={libraryError} onLoadQuiz={handleLoadQuiz} onDeleteQuiz={deleteQuiz} + onRetry={retryLibrary} /> ); diff --git a/components/QuizLibrary.tsx b/components/QuizLibrary.tsx index 9a0e4b7..ba2ce3c 100644 --- a/components/QuizLibrary.tsx +++ b/components/QuizLibrary.tsx @@ -8,9 +8,12 @@ interface QuizLibraryProps { onClose: () => void; quizzes: QuizListItem[]; loading: boolean; + loadingQuizId: string | null; + deletingQuizId: string | null; error: string | null; onLoadQuiz: (id: string) => void; onDeleteQuiz: (id: string) => void; + onRetry: () => void; } export const QuizLibrary: React.FC = ({ @@ -18,28 +21,36 @@ export const QuizLibrary: React.FC = ({ onClose, quizzes, loading, + loadingQuizId, + deletingQuizId, error, onLoadQuiz, onDeleteQuiz, + onRetry, }) => { - const [deletingId, setDeletingId] = useState(null); + const [confirmDeleteId, setConfirmDeleteId] = useState(null); + const isAnyOperationInProgress = loading || !!loadingQuizId || !!deletingQuizId; const handleDeleteClick = (e: React.MouseEvent, id: string) => { e.stopPropagation(); - setDeletingId(id); + setConfirmDeleteId(id); }; - const confirmDelete = (e: React.MouseEvent) => { + const confirmDelete = async (e: React.MouseEvent) => { e.stopPropagation(); - if (deletingId) { - onDeleteQuiz(deletingId); - setDeletingId(null); + if (confirmDeleteId) { + try { + await onDeleteQuiz(confirmDeleteId); + setConfirmDeleteId(null); + } catch { + setConfirmDeleteId(null); + } } }; const cancelDelete = (e: React.MouseEvent) => { e.stopPropagation(); - setDeletingId(null); + setConfirmDeleteId(null); }; const formatDate = (dateString: string) => { @@ -95,8 +106,14 @@ export const QuizLibrary: React.FC = ({ )} {!loading && error && ( -
- {error} +
+

{error}

+
)} @@ -116,8 +133,8 @@ export const QuizLibrary: React.FC = ({ layout initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - className="group bg-white p-4 rounded-2xl border-2 border-gray-100 hover:border-theme-primary hover:shadow-md transition-all cursor-pointer relative overflow-hidden" - onClick={() => onLoadQuiz(quiz.id)} + className={`group bg-white p-4 rounded-2xl border-2 border-gray-100 hover:border-theme-primary hover:shadow-md transition-all relative overflow-hidden ${isAnyOperationInProgress ? 'cursor-not-allowed opacity-70' : 'cursor-pointer'}`} + onClick={() => !isAnyOperationInProgress && onLoadQuiz(quiz.id)} >
@@ -147,17 +164,27 @@ export const QuizLibrary: React.FC = ({
- {deletingId === quiz.id ? ( + {loadingQuizId === quiz.id ? ( +
+ +
+ ) : deletingQuizId === quiz.id ? ( +
+ +
+ ) : confirmDeleteId === quiz.id ? (
e.stopPropagation()}> @@ -166,7 +193,8 @@ export const QuizLibrary: React.FC = ({ <>