216 lines
10 KiB
TypeScript
216 lines
10 KiB
TypeScript
'use client'
|
|
import {X} from 'lucide-react';
|
|
import IconButton from "@/components/ui/IconButton";
|
|
import React, {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
|
|
import {apiDelete, apiPost} from "@/lib/api/client";
|
|
import {fetch} from "@tauri-apps/plugin-http";
|
|
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
|
import {BookContext, BookContextProps} from "@/context/BookContext";
|
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
|
import TextInput from "@/components/form/TextInput";
|
|
import TextAreaInput from "@/components/form/TextAreaInput";
|
|
import InputField from "@/components/form/InputField";
|
|
import NumberInput from "@/components/form/NumberInput";
|
|
import DatePicker from "@/components/form/DatePicker";
|
|
import {configs, isDesktop} from "@/lib/configs";
|
|
import * as tauri from '@/lib/tauri';
|
|
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
|
import {BookProps} from "@/lib/types/book";
|
|
import {SettingRef} from "@/lib/types/settings";
|
|
import ImageDropZone from "@/components/form/ImageDropZone";
|
|
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
|
import {SyncedBook} from "@/lib/types/synced-book";
|
|
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
|
|
|
function BasicInformationSetting(_props: object, ref: React.ForwardedRef<SettingRef>): React.JSX.Element {
|
|
const t = useTranslations();
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
|
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
|
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
|
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
|
const bookId: string = book?.bookId ? book?.bookId.toString() : '';
|
|
|
|
const [currentImage, setCurrentImage] = useState<string>(book?.coverImage ?? '');
|
|
const [title, setTitle] = useState<string>(book?.title ? book?.title : '');
|
|
const [subTitle, setSubTitle] = useState<string>(book?.subTitle ? book?.subTitle : '');
|
|
const [summary, setSummary] = useState<string>(book?.summary ? book?.summary : '');
|
|
const [publicationDate, setPublicationDate] = useState<string>(book?.publicationDate ? book?.publicationDate : '');
|
|
const [wordCount, setWordCount] = useState<number>(book?.desiredWordCount ? book?.desiredWordCount : 0);
|
|
|
|
useImperativeHandle(ref, function (): SettingRef {
|
|
return {
|
|
handleSave: handleSave
|
|
};
|
|
});
|
|
|
|
async function handleCoverImageChange(file: File): Promise<void> {
|
|
const formData: FormData = new FormData();
|
|
formData.append('bookId', bookId);
|
|
formData.append('picture', file);
|
|
|
|
try {
|
|
const query: Response = await fetch(
|
|
configs.apiUrl + `book/cover?bookid=${bookId}&lang=${lang}&plateforme=desktop`,
|
|
{
|
|
method: "POST",
|
|
headers: {'Authorization': `Bearer ${userToken}`},
|
|
body: formData,
|
|
}
|
|
);
|
|
|
|
const contentType: string = query.headers.get('content-type') || 'image/jpeg';
|
|
const blob: Blob = new Blob([await query.arrayBuffer()], {type: contentType});
|
|
const reader: FileReader = new FileReader();
|
|
|
|
reader.onloadend = function (): void {
|
|
if (typeof reader.result === 'string') {
|
|
setCurrentImage(reader.result);
|
|
}
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('basicInformationSetting.error.unknown'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleRemoveCurrentImage(): Promise<void> {
|
|
try {
|
|
const response: boolean = await apiDelete<boolean>(`book/cover/delete`, {
|
|
bookId: bookId
|
|
}, userToken, lang);
|
|
if (!response) {
|
|
errorMessage(t('basicInformationSetting.error.removeCover'));
|
|
}
|
|
setCurrentImage('');
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('basicInformationSetting.error.unknown'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleSave(): Promise<void> {
|
|
if (!title) {
|
|
errorMessage(t('basicInformationSetting.error.titleRequired'));
|
|
return;
|
|
}
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.updateBookBasicInfo({
|
|
bookId: bookId,
|
|
title: title,
|
|
subTitle: subTitle,
|
|
summary: summary,
|
|
publicationDate: publicationDate,
|
|
wordCount: wordCount,
|
|
});
|
|
} else {
|
|
const basicInfoData = {
|
|
title, subTitle, summary, publicationDate, wordCount, bookId
|
|
};
|
|
response = await apiPost<boolean>('book/basic-information', basicInfoData, userToken, lang);
|
|
|
|
if (isDesktop && localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
|
addToQueue('update_book_basic_info', basicInfoData);
|
|
}
|
|
}
|
|
if (!response) {
|
|
errorMessage(t('basicInformationSetting.error.update'));
|
|
return;
|
|
}
|
|
if (!book) {
|
|
errorMessage(t('basicInformationSetting.error.unknown'));
|
|
return;
|
|
}
|
|
const updatedBook: BookProps = {
|
|
...book,
|
|
title: title,
|
|
subTitle: subTitle,
|
|
summary: summary,
|
|
publicationDate: publicationDate,
|
|
desiredWordCount: wordCount,
|
|
};
|
|
setBook(updatedBook);
|
|
successMessage(t('basicInformationSetting.success.update'));
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('basicInformationSetting.error.unknown'));
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InputField fieldName={t('basicInformationSetting.fields.title')} input={<TextInput
|
|
value={title}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>): void => setTitle(e.target.value)}
|
|
placeholder={t('basicInformationSetting.fields.titlePlaceholder')}
|
|
/>}/>
|
|
<InputField fieldName={t('basicInformationSetting.fields.subtitle')} input={<TextInput
|
|
value={subTitle}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>): void => setSubTitle(e.target.value)}
|
|
placeholder={t('basicInformationSetting.fields.subtitlePlaceholder')}
|
|
/>}/>
|
|
</div>
|
|
|
|
<InputField fieldName={t('basicInformationSetting.fields.summary')} input={<TextAreaInput
|
|
value={summary}
|
|
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setSummary(e.target.value)}
|
|
placeholder={t('basicInformationSetting.fields.summaryPlaceholder')}
|
|
/>}/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<InputField fieldName={t('basicInformationSetting.fields.publicationDate')} input={
|
|
<DatePicker
|
|
date={publicationDate}
|
|
setDate={(e: ChangeEvent<HTMLInputElement>): void => setPublicationDate(e.target.value)}
|
|
/>
|
|
}/>
|
|
<InputField fieldName={t('basicInformationSetting.fields.wordCount')} input={
|
|
<NumberInput value={wordCount} setValue={setWordCount}
|
|
placeholder={t('basicInformationSetting.fields.wordCountPlaceholder')}/>
|
|
}/>
|
|
</div>
|
|
|
|
<div>
|
|
{currentImage ? (
|
|
<div className="flex justify-center">
|
|
<div className="relative w-40">
|
|
<img src={currentImage} alt={t('basicInformationSetting.fields.coverImageAlt')}
|
|
className="rounded-lg border border-secondary w-full h-auto"/>
|
|
<div className="absolute -top-2 -right-2">
|
|
<IconButton icon={X} variant="danger" size="sm"
|
|
onClick={handleRemoveCurrentImage}/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ImageDropZone
|
|
onFileSelect={handleCoverImageChange}
|
|
label={t('basicInformationSetting.fields.coverImage')}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default forwardRef<SettingRef, object>(BasicInformationSetting); |