diff options
Diffstat (limited to 'src/client/components')
| -rw-r--r-- | src/client/components/CreateCardModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/CreateDeckModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/CreateNoteModal.tsx | 8 | ||||
| -rw-r--r-- | src/client/components/CreateNoteTypeModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/DeleteCardModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/DeleteDeckModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/DeleteNoteModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/DeleteNoteTypeModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/EditCardModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/EditDeckModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/EditNoteModal.tsx | 8 | ||||
| -rw-r--r-- | src/client/components/EditNoteTypeModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/ImportNotesModal.tsx | 6 | ||||
| -rw-r--r-- | src/client/components/SyncStatusIndicator.tsx | 6 |
14 files changed, 74 insertions, 14 deletions
diff --git a/src/client/components/CreateCardModal.tsx b/src/client/components/CreateCardModal.tsx index 3913e82..8dbaa79 100644 --- a/src/client/components/CreateCardModal.tsx +++ b/src/client/components/CreateCardModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface CreateCardModalProps { isOpen: boolean; @@ -18,6 +20,7 @@ export function CreateCardModal({ const [back, setBack] = useState(""); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const resetForm = () => { setFront(""); @@ -163,7 +166,8 @@ export function CreateCardModal({ </button> <button type="submit" - disabled={isSubmitting || !isFormValid} + disabled={isSubmitting || !isFormValid || !isOnline} + title={!isOnline ? "Reconnect to create a card" : undefined} 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 ? "Creating..." : "Create Card"} diff --git a/src/client/components/CreateDeckModal.tsx b/src/client/components/CreateDeckModal.tsx index 4541a68..34d46e7 100644 --- a/src/client/components/CreateDeckModal.tsx +++ b/src/client/components/CreateDeckModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface CreateDeckModalProps { isOpen: boolean; @@ -16,6 +18,7 @@ export function CreateDeckModal({ const [description, setDescription] = useState(""); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const resetForm = () => { setName(""); @@ -160,7 +163,8 @@ export function CreateDeckModal({ </button> <button type="submit" - disabled={isSubmitting || !name.trim()} + disabled={isSubmitting || !name.trim() || !isOnline} + title={!isOnline ? "Reconnect to create a deck" : undefined} 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 ? "Creating..." : "Create Deck"} diff --git a/src/client/components/CreateNoteModal.tsx b/src/client/components/CreateNoteModal.tsx index cc39bf6..f3809ea 100644 --- a/src/client/components/CreateNoteModal.tsx +++ b/src/client/components/CreateNoteModal.tsx @@ -1,7 +1,9 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useAtomValue } from "jotai"; import { type FormEvent, useCallback, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface NoteField { id: string; @@ -49,6 +51,7 @@ export function CreateNoteModal({ const [isLoadingNoteType, setIsLoadingNoteType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [hasLoadedNoteTypes, setHasLoadedNoteTypes] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const fetchNoteTypeDetails = useCallback(async (noteTypeId: string) => { setIsLoadingNoteType(true); @@ -346,7 +349,10 @@ export function CreateNoteModal({ </button> <button type="submit" - disabled={isSubmitting || !isFormValid || isLoading} + disabled={ + isSubmitting || !isFormValid || isLoading || !isOnline + } + title={!isOnline ? "Reconnect to create a note" : undefined} 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 ? "Creating..." : "Create Note"} diff --git a/src/client/components/CreateNoteTypeModal.tsx b/src/client/components/CreateNoteTypeModal.tsx index 4c3b232..bbd43a1 100644 --- a/src/client/components/CreateNoteTypeModal.tsx +++ b/src/client/components/CreateNoteTypeModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface CreateNoteTypeModalProps { isOpen: boolean; @@ -18,6 +20,7 @@ export function CreateNoteTypeModal({ const [isReversible, setIsReversible] = useState(false); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const resetForm = () => { setName(""); @@ -197,7 +200,8 @@ export function CreateNoteTypeModal({ </button> <button type="submit" - disabled={isSubmitting || !name.trim()} + disabled={isSubmitting || !name.trim() || !isOnline} + title={!isOnline ? "Reconnect to create" : undefined} 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 ? "Creating..." : "Create"} diff --git a/src/client/components/DeleteCardModal.tsx b/src/client/components/DeleteCardModal.tsx index d9cf098..99514be 100644 --- a/src/client/components/DeleteCardModal.tsx +++ b/src/client/components/DeleteCardModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface Card { id: string; @@ -23,6 +25,7 @@ export function DeleteCardModal({ }: DeleteCardModalProps) { const [error, setError] = useState<string | null>(null); const [isDeleting, setIsDeleting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const handleClose = () => { setError(null); @@ -138,7 +141,8 @@ export function DeleteCardModal({ <button type="button" onClick={handleDelete} - disabled={isDeleting} + disabled={isDeleting || !isOnline} + title={!isOnline ? "Reconnect to delete" : undefined} className="px-4 py-2 bg-error hover:bg-error/90 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]" > {isDeleting ? "Deleting..." : "Delete"} diff --git a/src/client/components/DeleteDeckModal.tsx b/src/client/components/DeleteDeckModal.tsx index edc6093..954431e 100644 --- a/src/client/components/DeleteDeckModal.tsx +++ b/src/client/components/DeleteDeckModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface Deck { id: string; @@ -21,6 +23,7 @@ export function DeleteDeckModal({ }: DeleteDeckModalProps) { const [error, setError] = useState<string | null>(null); const [isDeleting, setIsDeleting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const handleClose = () => { setError(null); @@ -129,7 +132,8 @@ export function DeleteDeckModal({ <button type="button" onClick={handleDelete} - disabled={isDeleting} + disabled={isDeleting || !isOnline} + title={!isOnline ? "Reconnect to delete" : undefined} className="px-4 py-2 bg-error hover:bg-error/90 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]" > {isDeleting ? "Deleting..." : "Delete"} diff --git a/src/client/components/DeleteNoteModal.tsx b/src/client/components/DeleteNoteModal.tsx index 5d81fdc..3ed22ec 100644 --- a/src/client/components/DeleteNoteModal.tsx +++ b/src/client/components/DeleteNoteModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface DeleteNoteModalProps { isOpen: boolean; @@ -18,6 +20,7 @@ export function DeleteNoteModal({ }: DeleteNoteModalProps) { const [error, setError] = useState<string | null>(null); const [isDeleting, setIsDeleting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const handleClose = () => { setError(null); @@ -127,7 +130,8 @@ export function DeleteNoteModal({ <button type="button" onClick={handleDelete} - disabled={isDeleting} + disabled={isDeleting || !isOnline} + title={!isOnline ? "Reconnect to delete" : undefined} className="px-4 py-2 bg-error hover:bg-error/90 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]" > {isDeleting ? "Deleting..." : "Delete"} diff --git a/src/client/components/DeleteNoteTypeModal.tsx b/src/client/components/DeleteNoteTypeModal.tsx index db93482..2fbf808 100644 --- a/src/client/components/DeleteNoteTypeModal.tsx +++ b/src/client/components/DeleteNoteTypeModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface NoteType { id: string; @@ -21,6 +23,7 @@ export function DeleteNoteTypeModal({ }: DeleteNoteTypeModalProps) { const [error, setError] = useState<string | null>(null); const [isDeleting, setIsDeleting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const handleClose = () => { setError(null); @@ -129,7 +132,8 @@ export function DeleteNoteTypeModal({ <button type="button" onClick={handleDelete} - disabled={isDeleting} + disabled={isDeleting || !isOnline} + title={!isOnline ? "Reconnect to delete" : undefined} className="px-4 py-2 bg-error hover:bg-error/90 text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]" > {isDeleting ? "Deleting..." : "Delete"} diff --git a/src/client/components/EditCardModal.tsx b/src/client/components/EditCardModal.tsx index 726a003..288bfd6 100644 --- a/src/client/components/EditCardModal.tsx +++ b/src/client/components/EditCardModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface Card { id: string; @@ -26,6 +28,7 @@ export function EditCardModal({ const [back, setBack] = useState(""); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); // Sync form state when card changes useEffect(() => { @@ -164,7 +167,8 @@ export function EditCardModal({ </button> <button type="submit" - disabled={isSubmitting || !isFormValid} + disabled={isSubmitting || !isFormValid || !isOnline} + title={!isOnline ? "Reconnect to save changes" : undefined} 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"} diff --git a/src/client/components/EditDeckModal.tsx b/src/client/components/EditDeckModal.tsx index 9a79de8..e9c2b7b 100644 --- a/src/client/components/EditDeckModal.tsx +++ b/src/client/components/EditDeckModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useCallback, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface Deck { id: string; @@ -35,6 +37,7 @@ export function EditDeckModal({ const [isLoadingNoteTypes, setIsLoadingNoteTypes] = useState(false); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const fetchNoteTypes = useCallback(async () => { setIsLoadingNoteTypes(true); @@ -216,7 +219,8 @@ export function EditDeckModal({ </button> <button type="submit" - disabled={isSubmitting || !name.trim()} + disabled={isSubmitting || !name.trim() || !isOnline} + title={!isOnline ? "Reconnect to save changes" : undefined} 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"} diff --git a/src/client/components/EditNoteModal.tsx b/src/client/components/EditNoteModal.tsx index ac22332..cd2c58c 100644 --- a/src/client/components/EditNoteModal.tsx +++ b/src/client/components/EditNoteModal.tsx @@ -1,7 +1,9 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useAtomValue } from "jotai"; import { type FormEvent, useCallback, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface NoteField { id: string; @@ -54,6 +56,7 @@ export function EditNoteModal({ const [isLoadingNote, setIsLoadingNote] = useState(false); const [isLoadingNoteType, setIsLoadingNoteType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); const fetchNoteTypeDetails = useCallback(async (noteTypeId: string) => { setIsLoadingNoteType(true); @@ -297,7 +300,10 @@ export function EditNoteModal({ </button> <button type="submit" - disabled={isSubmitting || !isFormValid || isLoading} + disabled={ + isSubmitting || !isFormValid || isLoading || !isOnline + } + title={!isOnline ? "Reconnect to save changes" : undefined} 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"} diff --git a/src/client/components/EditNoteTypeModal.tsx b/src/client/components/EditNoteTypeModal.tsx index 27ef5d8..5916ff0 100644 --- a/src/client/components/EditNoteTypeModal.tsx +++ b/src/client/components/EditNoteTypeModal.tsx @@ -1,5 +1,7 @@ +import { useAtomValue } from "jotai"; import { type FormEvent, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; interface NoteType { id: string; @@ -28,6 +30,7 @@ export function EditNoteTypeModal({ const [isReversible, setIsReversible] = useState(false); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false); + const isOnline = useAtomValue(isOnlineAtom); // Sync form state when noteType changes useEffect(() => { @@ -208,7 +211,8 @@ export function EditNoteTypeModal({ </button> <button type="submit" - disabled={isSubmitting || !name.trim()} + disabled={isSubmitting || !name.trim() || !isOnline} + title={!isOnline ? "Reconnect to save changes" : undefined} 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"} diff --git a/src/client/components/ImportNotesModal.tsx b/src/client/components/ImportNotesModal.tsx index d3a2c0c..a38ac8f 100644 --- a/src/client/components/ImportNotesModal.tsx +++ b/src/client/components/ImportNotesModal.tsx @@ -5,8 +5,10 @@ import { faSpinner, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useAtomValue } from "jotai"; import { type ChangeEvent, useCallback, useEffect, useState } from "react"; import { ApiClientError, apiClient } from "../api"; +import { isOnlineAtom } from "../atoms"; import { parseCSV } from "../utils/csvParser"; interface NoteField { @@ -64,6 +66,7 @@ export function ImportNotesModal({ }: ImportNotesModalProps) { const [phase, setPhase] = useState<ImportPhase>("upload"); const [error, setError] = useState<string | null>(null); + const isOnline = useAtomValue(isOnlineAtom); const [noteTypes, setNoteTypes] = useState<NoteType[]>([]); const [validatedRows, setValidatedRows] = useState<ValidatedRow[]>([]); const [validationErrors, setValidationErrors] = useState<ValidationError[]>( @@ -490,7 +493,8 @@ export function ImportNotesModal({ <button type="button" onClick={handleImport} - disabled={validatedRows.length === 0} + disabled={validatedRows.length === 0 || !isOnline} + title={!isOnline ? "Reconnect to import notes" : undefined} 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" > Import {validatedRows.length} Note(s) diff --git a/src/client/components/SyncStatusIndicator.tsx b/src/client/components/SyncStatusIndicator.tsx index 4bb3ff5..c517b76 100644 --- a/src/client/components/SyncStatusIndicator.tsx +++ b/src/client/components/SyncStatusIndicator.tsx @@ -101,11 +101,15 @@ export function SyncStatusIndicator() { ); }; + const titleText = !isOnline + ? "Showing cached data — changes will sync when you're back online" + : lastError || undefined; + return ( <div data-testid="sync-status-indicator" className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium transition-colors ${getStatusStyles()}`} - title={lastError || undefined} + title={titleText} > {getStatusIcon()} <span>{getStatusText()}</span> |
