Introduce local fallback for book creation and improve error handling
- Added support for creating books locally when the cloud limit is reached. - Enhanced error handling in `AddNewBookForm` with `ApiError` and fallback logic for local book creation. - Implemented `BookTypeLimit` to manage type-specific book limits with visual indicators in `BookList`. - Refactored `TombstoneRecord` to standardize naming conventions for better API compatibility. - Updated `useSyncSeries` and `useSyncBooks` to handle empty tombstones gracefully. - Updated locales with new translations for fallback and error messaging.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from "react";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {apiPost} from "@/lib/api/client";
|
||||
import {ApiError, apiPost} from "@/lib/api/client";
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {Book, BookOpen, Calendar, FileText, Info, Pencil} from "lucide-react";
|
||||
import {AlertTriangle, Book, BookOpen, Calendar, FileText, HardDrive, Info, Pencil} from "lucide-react";
|
||||
import SelectBox, {SelectBoxProps} from "@/components/form/SelectBox";
|
||||
import {bookTypes} from "@/lib/constants/book";
|
||||
import InputField from "@/components/form/InputField";
|
||||
@@ -32,7 +32,7 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {session, setSession}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {setServerSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||
const {setServerSyncedBooks, setLocalOnlyBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [subtitle, setSubtitle] = useState<string>('');
|
||||
@@ -43,6 +43,7 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
|
||||
const [isAddingBook, setIsAddingBook] = useState<boolean>(false);
|
||||
const [bookTypeHint, setBookTypeHint] = useState<boolean>(false);
|
||||
const [showLocalFallback, setShowLocalFallback] = useState<boolean>(false);
|
||||
|
||||
const token: string = session?.accessToken ?? '';
|
||||
|
||||
@@ -154,9 +155,61 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
aiGuideLine: null
|
||||
};
|
||||
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, book])
|
||||
|
||||
|
||||
setIsAddingBook(false);
|
||||
setCloseForm(false)
|
||||
} catch (e: unknown) {
|
||||
const apiError = e as ApiError;
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('addNewBookForm.error.addingBook'));
|
||||
}
|
||||
if (apiError?.statusCode === 409 && isDesktop) {
|
||||
setShowLocalFallback(true);
|
||||
}
|
||||
setIsAddingBook(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateLocalBook(): Promise<void> {
|
||||
setIsAddingBook(true);
|
||||
setShowLocalFallback(false);
|
||||
try {
|
||||
const bookId: string = await tauri.createBook({
|
||||
title: title,
|
||||
subTitle: subtitle,
|
||||
type: selectedBookType,
|
||||
summary: summary,
|
||||
desiredReleaseDate: publicationDate,
|
||||
desiredWordCount: wordCount,
|
||||
});
|
||||
if (!bookId) {
|
||||
errorMessage(t('addNewBookForm.error.addingBook'));
|
||||
setIsAddingBook(false);
|
||||
return;
|
||||
}
|
||||
const book: SyncedBook = {
|
||||
id: bookId,
|
||||
type: selectedBookType,
|
||||
title: title,
|
||||
subTitle: subtitle,
|
||||
seriesId: null,
|
||||
lastUpdate: new Date().getTime() / 1000,
|
||||
chapters: [],
|
||||
characters: [],
|
||||
locations: [],
|
||||
worlds: [],
|
||||
incidents: [],
|
||||
plotPoints: [],
|
||||
issues: [],
|
||||
actSummaries: [],
|
||||
guideLine: null,
|
||||
aiGuideLine: null
|
||||
};
|
||||
setLocalOnlyBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, book]);
|
||||
setIsAddingBook(false);
|
||||
setCloseForm(false);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
@@ -166,7 +219,7 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
setIsAddingBook(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function maxWordsCountHint(): MinMax {
|
||||
switch (selectedBookType) {
|
||||
case 'short':
|
||||
@@ -212,16 +265,35 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => setCloseForm(false)}>{t("common.cancel")}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleAddBook}
|
||||
isLoading={isAddingBook}
|
||||
loadingText={t("addNewBookForm.adding")}
|
||||
icon={Book}
|
||||
>{t("addNewBookForm.add")}</Button>
|
||||
{showLocalFallback ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleCreateLocalBook}
|
||||
isLoading={isAddingBook}
|
||||
loadingText={t("addNewBookForm.adding")}
|
||||
icon={HardDrive}
|
||||
>{t("addNewBookForm.error.saveLocally")}</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleAddBook}
|
||||
isLoading={isAddingBook}
|
||||
loadingText={t("addNewBookForm.adding")}
|
||||
icon={Book}
|
||||
>{t("addNewBookForm.add")}</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{showLocalFallback && (
|
||||
<div className="flex items-start gap-3 bg-warning/10 border border-warning/30 rounded-xl p-4 mb-4">
|
||||
<AlertTriangle strokeWidth={1.75} className="w-5 h-5 text-warning shrink-0 mt-0.5"/>
|
||||
<div>
|
||||
<p className="text-text-primary font-medium text-sm">{t("addNewBookForm.error.limitReached")}</p>
|
||||
<p className="text-muted text-sm mt-1">{t("addNewBookForm.error.localFallbackDescription")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<InputField icon={BookOpen} fieldName={t("addNewBookForm.type")} input={
|
||||
<SelectBox
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setSelectedBookType(e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user