Add components for Act management and integrate Electron setup
This commit is contained in:
295
components/book/AddNewBookForm.tsx
Normal file
295
components/book/AddNewBookForm.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
'use client'
|
||||
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useEffect, useRef, useState} from "react";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import System from "@/lib/models/System";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBook,
|
||||
faBookOpen,
|
||||
faCalendarAlt,
|
||||
faFileWord,
|
||||
faInfo,
|
||||
faPencilAlt,
|
||||
faX
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import {BookProps, bookTypes} from "@/lib/models/Book";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import DatePicker from "@/components/form/DatePicker";
|
||||
import NumberInput from "@/components/form/NumberInput";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import CancelButton from "@/components/form/CancelButton";
|
||||
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
|
||||
import GuideTour, {GuideStep} from "@/components/GuideTour";
|
||||
import {UserProps} from "@/lib/models/User";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface MinMax {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<SetStateAction<boolean>> }) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {session, setSession} = useContext(SessionContext);
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
const modalRef: React.RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [title, setTitle] = useState<string>('');
|
||||
const [subtitle, setSubtitle] = useState<string>('');
|
||||
const [summary, setSummary] = useState<string>('');
|
||||
const [publicationDate, setPublicationDate] = useState<string>('');
|
||||
const [wordCount, setWordCount] = useState<number>(0);
|
||||
const [selectedBookType, setSelectedBookType] = useState<string>('');
|
||||
|
||||
const [isAddingBook, setIsAddingBook] = useState<boolean>(false);
|
||||
const [bookTypeHint, setBookTypeHint] = useState<boolean>(false);
|
||||
|
||||
const token: string = session?.accessToken ?? '';
|
||||
|
||||
const bookTypesHint: GuideStep[] = [{
|
||||
id: 0,
|
||||
x: 80,
|
||||
y: 50,
|
||||
title: t("addNewBookForm.bookTypeHint.title"),
|
||||
content: (
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto custom-scrollbar">
|
||||
<div className="space-y-3">
|
||||
<div className="border-l-4 border-primary pl-4 bg-secondary/10 p-3 rounded-r-xl">
|
||||
<h4 className="font-semibold text-lg text-text-primary mb-1">{t("addNewBookForm.bookTypeHint.nouvelle.title")}</h4>
|
||||
<p className="text-sm text-muted mb-2">{t("addNewBookForm.bookTypeHint.nouvelle.range")}</p>
|
||||
<p className="text-sm text-text-secondary">{t("addNewBookForm.bookTypeHint.nouvelle.description")}</p>
|
||||
</div>
|
||||
<div className="border-l-4 border-primary pl-4 bg-secondary/10 p-3 rounded-r-xl">
|
||||
<h4 className="font-semibold text-lg text-text-primary mb-1">{t("addNewBookForm.bookTypeHint.novelette.title")}</h4>
|
||||
<p className="text-sm text-muted mb-2">{t("addNewBookForm.bookTypeHint.novelette.range")}</p>
|
||||
<p className="text-sm text-text-secondary">{t("addNewBookForm.bookTypeHint.novelette.description")}</p>
|
||||
</div>
|
||||
<div className="border-l-4 border-primary pl-4 bg-secondary/10 p-3 rounded-r-xl">
|
||||
<h4 className="font-semibold text-lg text-text-primary mb-1">{t("addNewBookForm.bookTypeHint.novella.title")}</h4>
|
||||
<p className="text-sm text-muted mb-2">{t("addNewBookForm.bookTypeHint.novella.range")}</p>
|
||||
<p className="text-sm text-text-secondary">{t("addNewBookForm.bookTypeHint.novella.description")}</p>
|
||||
</div>
|
||||
<div className="border-l-4 border-primary pl-4 bg-secondary/10 p-3 rounded-r-xl">
|
||||
<h4 className="font-semibold text-lg text-text-primary mb-1">{t("addNewBookForm.bookTypeHint.chapbook.title")}</h4>
|
||||
<p className="text-sm text-muted mb-2">{t("addNewBookForm.bookTypeHint.chapbook.range")}</p>
|
||||
<p className="text-sm text-text-secondary">{t("addNewBookForm.bookTypeHint.chapbook.description")}</p>
|
||||
</div>
|
||||
<div className="border-l-4 border-primary pl-4 bg-secondary/10 p-3 rounded-r-xl">
|
||||
<h4 className="font-semibold text-lg text-text-primary mb-1">{t("addNewBookForm.bookTypeHint.roman.title")}</h4>
|
||||
<p className="text-sm text-muted mb-2">{t("addNewBookForm.bookTypeHint.roman.range")}</p>
|
||||
<p className="text-sm text-text-secondary">{t("addNewBookForm.bookTypeHint.roman.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-primary/10 border border-primary/30 p-4 rounded-xl">
|
||||
<p className="text-sm text-text-primary font-medium">
|
||||
{t("addNewBookForm.bookTypeHint.tip")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}]
|
||||
|
||||
useEffect((): () => void => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return (): void => {
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleAddBook(): Promise<void> {
|
||||
if (!title) {
|
||||
errorMessage(t('addNewBookForm.error.titleMissing'));
|
||||
return;
|
||||
} else {
|
||||
if (title.length < 2) {
|
||||
errorMessage(t('addNewBookForm.error.titleTooShort'));
|
||||
return;
|
||||
}
|
||||
if (title.length > 50) {
|
||||
errorMessage(t('addNewBookForm.error.titleTooLong'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (selectedBookType === '') {
|
||||
errorMessage(t('addNewBookForm.error.typeMissing'));
|
||||
return;
|
||||
}
|
||||
setIsAddingBook(true);
|
||||
try {
|
||||
const bookId: string = await System.authPostToServer<string>('book/add', {
|
||||
title: title,
|
||||
subTitle: subtitle,
|
||||
type: selectedBookType,
|
||||
summary: summary,
|
||||
serie: 0,
|
||||
publicationDate: publicationDate,
|
||||
desiredWordCount: wordCount,
|
||||
}, token, lang)
|
||||
if (!bookId) {
|
||||
errorMessage(t('addNewBookForm.error.addingBook'));
|
||||
setIsAddingBook(false);
|
||||
return;
|
||||
}
|
||||
const book: BookProps = {
|
||||
bookId: bookId,
|
||||
title,
|
||||
subTitle: subtitle,
|
||||
type: selectedBookType,
|
||||
summary, serie: 0,
|
||||
publicationDate,
|
||||
desiredWordCount: wordCount
|
||||
};
|
||||
setSession({
|
||||
...session,
|
||||
user: {
|
||||
...session.user as UserProps,
|
||||
books: [...((session.user as UserProps)?.books ?? []), book]
|
||||
}
|
||||
});
|
||||
setIsAddingBook(false);
|
||||
setCloseForm(false)
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('addNewBookForm.error.addingBook'));
|
||||
}
|
||||
setIsAddingBook(false);
|
||||
}
|
||||
}
|
||||
|
||||
function maxWordsCountHint(): MinMax {
|
||||
switch (selectedBookType) {
|
||||
case 'short':
|
||||
return {
|
||||
min: 1000,
|
||||
max: 7500,
|
||||
};
|
||||
case 'chapbook':
|
||||
return {
|
||||
min: 1000,
|
||||
max: 10000,
|
||||
};
|
||||
case 'novelette' :
|
||||
return {
|
||||
min: 7500,
|
||||
max: 17500,
|
||||
};
|
||||
case 'long' :
|
||||
return {
|
||||
min: 17500,
|
||||
max: 40000,
|
||||
};
|
||||
case 'novel' :
|
||||
return {
|
||||
min: 40000,
|
||||
max: 0,
|
||||
};
|
||||
default :
|
||||
return {
|
||||
min: 0,
|
||||
max: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center bg-black/60 z-50 backdrop-blur-md animate-fadeIn">
|
||||
<div ref={modalRef}
|
||||
className="bg-tertiary/95 backdrop-blur-sm text-text-primary rounded-2xl border border-secondary/50 shadow-2xl md:w-3/4 xl:w-1/4 lg:w-2/4 sm:w-11/12 max-h-[85vh] flex flex-col">
|
||||
<div className="flex justify-between items-center bg-primary px-6 py-4 rounded-t-2xl shadow-lg">
|
||||
<h2 className="flex items-center gap-3 font-['ADLaM_Display'] text-2xl text-text-primary">
|
||||
<FontAwesomeIcon icon={faBook} className="w-6 h-6"/>
|
||||
{t("addNewBookForm.title")}
|
||||
</h2>
|
||||
<button
|
||||
className="text-background hover:text-background w-10 h-10 rounded-xl hover:bg-white/20 transition-all duration-200 flex items-center justify-center hover:scale-110"
|
||||
onClick={(): void => setCloseForm(false)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5 overflow-y-auto flex-grow custom-scrollbar">
|
||||
<div className="space-y-6">
|
||||
<InputField icon={faBookOpen} fieldName={t("addNewBookForm.type")} input={
|
||||
<SelectBox
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => setSelectedBookType(e.target.value)}
|
||||
data={bookTypes.map((types: SelectBoxProps): SelectBoxProps => {
|
||||
return {
|
||||
value: types.value,
|
||||
label: t(types.label)
|
||||
}
|
||||
})} defaultValue={selectedBookType}
|
||||
placeholder={t("addNewBookForm.typePlaceholder")}/>
|
||||
} action={async (): Promise<void> => setBookTypeHint(true)} actionIcon={faInfo}/>
|
||||
<InputField icon={faPencilAlt} fieldName={t("addNewBookForm.bookTitle")} input={
|
||||
<TextInput value={title}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>): void => setTitle(e.target.value)}
|
||||
placeholder={t("addNewBookForm.bookTitlePlaceholder")}/>
|
||||
}/>
|
||||
{
|
||||
selectedBookType !== 'lyric' && (
|
||||
<InputField icon={faPencilAlt} fieldName={t("addNewBookForm.subtitle")} input={
|
||||
<TextInput value={subtitle}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>): void => setSubtitle(e.target.value)}
|
||||
placeholder={t("addNewBookForm.subtitlePlaceholder")}/>
|
||||
}/>
|
||||
)
|
||||
}
|
||||
|
||||
<InputField icon={faCalendarAlt} fieldName={t("addNewBookForm.publicationDate")} input={
|
||||
<DatePicker date={publicationDate}
|
||||
setDate={(e: React.ChangeEvent<HTMLInputElement>): void => setPublicationDate(e.target.value)}/>
|
||||
}/>
|
||||
|
||||
{
|
||||
selectedBookType !== 'lyric' && (
|
||||
<>
|
||||
<InputField icon={faFileWord} fieldName={t("addNewBookForm.wordGoal")}
|
||||
hint={selectedBookType && `${maxWordsCountHint().min.toLocaleString('fr-FR')} - ${maxWordsCountHint().max > 0 ? maxWordsCountHint().max.toLocaleString('fr-FR') : '∞'} ${t("addNewBookForm.words")}`}
|
||||
input={
|
||||
<NumberInput value={wordCount} setValue={setWordCount}
|
||||
placeholder={t("addNewBookForm.wordGoalPlaceholder")}/>
|
||||
}/>
|
||||
|
||||
<InputField
|
||||
icon={faFileWord}
|
||||
fieldName={t("addNewBookForm.summary")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={summary}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setSummary(e.target.value)}
|
||||
placeholder={t("addNewBookForm.summaryPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex justify-between items-center p-5 border-t border-secondary/50 bg-secondary/20 rounded-b-2xl">
|
||||
<div></div>
|
||||
<div className="flex gap-3">
|
||||
<CancelButton callBackFunction={() => setCloseForm(false)}/>
|
||||
<SubmitButtonWLoading callBackAction={handleAddBook} isLoading={isAddingBook}
|
||||
text={t("addNewBookForm.add")}
|
||||
loadingText={t("addNewBookForm.adding")} icon={faBook}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{bookTypeHint && <GuideTour stepId={0} steps={bookTypesHint} onClose={(): void => setBookTypeHint(false)}
|
||||
onComplete={async (): Promise<void> => setBookTypeHint(false)}/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
components/book/BookCard.tsx
Normal file
80
components/book/BookCard.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import {BookProps} from "@/lib/models/Book";
|
||||
import DeleteBook from "@/components/book/settings/DeleteBook";
|
||||
import ExportBook from "@/components/ExportBook";
|
||||
import {useTranslations} from "next-intl";
|
||||
|
||||
export default function BookCard(
|
||||
{
|
||||
book,
|
||||
onClickCallback,
|
||||
index
|
||||
}: {
|
||||
book: BookProps,
|
||||
onClickCallback: Function;
|
||||
index: number;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group bg-tertiary/90 backdrop-blur-sm rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 h-full border border-secondary/50 hover:border-primary/50 flex flex-col hover:scale-105">
|
||||
<div className="relative w-full h-[400px] sm:h-32 md:h-48 lg:h-64 xl:h-80 flex-shrink-0 overflow-hidden">
|
||||
<Link onClick={(): void => onClickCallback(book.bookId)} href={``}>
|
||||
{book.coverImage ? (
|
||||
<img
|
||||
src={book.coverImage}
|
||||
alt={book.title || t("bookCard.noCoverAlt")}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full h-full bg-gradient-to-br from-secondary via-secondary to-gray-dark flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-primary/5"></div>
|
||||
<span
|
||||
className="relative text-primary/80 text-4xl sm:text-5xl md:text-6xl font-['ADLaM_Display'] tracking-wider">
|
||||
{book.title.charAt(0).toUpperCase()}{t("bookCard.initialsSeparator")}{book.subTitle ? book.subTitle.charAt(0).toUpperCase() : ''}
|
||||
</span>
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent"></div>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-tertiary via-tertiary/50 to-transparent h-24"></div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex-1 flex flex-col justify-between">
|
||||
<div className="flex-1">
|
||||
<Link onClick={(): void => onClickCallback(book.bookId)} href={``}>
|
||||
<h3 className="text-text-primary text-center font-bold text-base mb-2 truncate group-hover:text-primary transition-colors tracking-wide">
|
||||
{book.title}
|
||||
</h3>
|
||||
</Link>
|
||||
<div className="flex items-center justify-center mb-3 h-5">
|
||||
{book.subTitle ? (
|
||||
<>
|
||||
<div className="h-px w-8 bg-primary/30"></div>
|
||||
<p className="text-muted text-center mx-2 text-xs italic truncate px-2">
|
||||
{book.subTitle}
|
||||
</p>
|
||||
<div className="h-px w-8 bg-primary/30"></div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pt-3 border-t border-secondary/30">
|
||||
<span
|
||||
className="bg-primary/10 text-primary text-xs px-3 py-1 rounded-full font-medium border border-primary/30"></span>
|
||||
<div className="flex items-center gap-1" {...index === 0 && {'data-guide': 'bottom-book-card'}}>
|
||||
<ExportBook bookTitle={book.title} bookId={book.bookId}/>
|
||||
<DeleteBook bookId={book.bookId}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
components/book/BookCardSkeleton.tsx
Normal file
26
components/book/BookCardSkeleton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
export default function BookCardSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg h-full border border-secondary/50 flex flex-col animate-pulse">
|
||||
<div className="relative w-full h-[400px] sm:h-32 md:h-48 lg:h-64 xl:h-80 flex-shrink-0">
|
||||
<div className="w-full h-full bg-secondary/30 rounded-t-xl"></div>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background to-transparent h-20"></div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="h-4 bg-secondary/30 rounded-lg mb-2 w-3/4 mx-auto"></div>
|
||||
<div className="h-3 bg-secondary/20 rounded-lg w-1/2 mx-auto"></div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<div className="h-6 bg-secondary/30 rounded-full w-16"></div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-8 w-8 bg-secondary/30 rounded-lg"></div>
|
||||
<div className="h-8 w-8 bg-secondary/30 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
291
components/book/BookList.tsx
Normal file
291
components/book/BookList.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import SearchBook from "./SearchBook";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBook, faDownload, faGear, faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import Book, {BookListProps, BookProps} from "@/lib/models/Book";
|
||||
import BookCard from "@/components/book/BookCard";
|
||||
import BookCardSkeleton from "@/components/book/BookCardSkeleton";
|
||||
import GuideTour, {GuideStep} from "@/components/GuideTour";
|
||||
import User from "@/lib/models/User";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
export default function BookList() {
|
||||
const {session, setSession} = useContext(SessionContext);
|
||||
const accessToken: string = session?.accessToken || '';
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
const {setBook} = useContext(BookContext);
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
|
||||
const [isLoadingBooks, setIsLoadingBooks] = useState<boolean>(true);
|
||||
|
||||
const [bookGuide, setBookGuide] = useState<boolean>(false);
|
||||
|
||||
const bookGuideSteps: GuideStep[] = [
|
||||
{
|
||||
id: 0,
|
||||
targetSelector: '[data-guide="book-category"]',
|
||||
position: 'left',
|
||||
highlightRadius: -200,
|
||||
title: `${t("bookList.guideStep0Title")} ${session.user?.name}`,
|
||||
content: (
|
||||
<div>
|
||||
<p>{t("bookList.guideStep0Content")}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
targetSelector: '[data-guide="book-card"]',
|
||||
position: 'left',
|
||||
title: t("bookList.guideStep1Title"),
|
||||
content: (
|
||||
<div>
|
||||
<p>{t("bookList.guideStep1Content")}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
targetSelector: '[data-guide="bottom-book-card"]',
|
||||
position: 'left',
|
||||
title: t("bookList.guideStep2Title"),
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faGear} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentGear")}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faDownload} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentDownload")}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faTrash} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentTrash")}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
useEffect((): void => {
|
||||
if (groupedBooks && Object.keys(groupedBooks).length > 0 && User.guideTourDone(session.user?.guideTour || [], 'new-first-book')) {
|
||||
setBookGuide(true);
|
||||
}
|
||||
}, [groupedBooks]);
|
||||
|
||||
useEffect((): void => {
|
||||
getBooks().then()
|
||||
}, [session.user?.books]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (accessToken) getBooks().then();
|
||||
}, [accessToken]);
|
||||
|
||||
async function handleFirstBookGuide(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPostToServer<boolean>(
|
||||
'logs/tour',
|
||||
{plateforme: 'web', tour: 'new-first-book'},
|
||||
session.accessToken, lang
|
||||
);
|
||||
if (response) {
|
||||
setSession(User.setNewGuideTour(session, 'new-first-book'));
|
||||
setBookGuide(false);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorBookCreate"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getBooks(): Promise<void> {
|
||||
setIsLoadingBooks(true);
|
||||
try {
|
||||
const bookResponse: BookListProps[] = await System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang);
|
||||
if (bookResponse) {
|
||||
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
|
||||
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
|
||||
const categoryLabel: string = Book.getBookTypeLabel(book.type);
|
||||
const transformedBook: BookProps = {
|
||||
bookId: book.id,
|
||||
type: categoryLabel,
|
||||
title: book.title,
|
||||
subTitle: book.subTitle,
|
||||
summary: book.summary,
|
||||
serie: book.serieId,
|
||||
publicationDate: book.desiredReleaseDate,
|
||||
desiredWordCount: book.desiredWordCount,
|
||||
totalWordCount: 0,
|
||||
coverImage: imageDataUrl,
|
||||
};
|
||||
if (!groups[t(categoryLabel)]) {
|
||||
groups[t(categoryLabel)] = [];
|
||||
}
|
||||
groups[t(categoryLabel)].push(transformedBook);
|
||||
return groups;
|
||||
}, {});
|
||||
setGroupedBooks(booksByType);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorBooksFetch"));
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingBooks(false);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredGroupedBooks: Record<string, BookProps[]> = Object.entries(groupedBooks).reduce(
|
||||
(acc: Record<string, BookProps[]>, [category, books]: [string, BookProps[]]): Record<string, BookProps[]> => {
|
||||
const filteredBooks: BookProps[] = books.filter((book: BookProps): boolean =>
|
||||
book.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
if (filteredBooks.length > 0) {
|
||||
acc[category] = filteredBooks;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
async function getBook(bookId: string): Promise<void> {
|
||||
try {
|
||||
const bookResponse: BookListProps = await System.authGetQueryToServer<BookListProps>(
|
||||
`book/basic-information`,
|
||||
accessToken,
|
||||
lang,
|
||||
{id: bookId}
|
||||
);
|
||||
if (!bookResponse) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
return;
|
||||
}
|
||||
if (setBook) {
|
||||
setBook({
|
||||
bookId: bookId,
|
||||
title: bookResponse?.title || '',
|
||||
subTitle: bookResponse?.subTitle || '',
|
||||
summary: bookResponse?.summary || '',
|
||||
type: bookResponse?.type || '',
|
||||
serie: bookResponse?.serieId,
|
||||
publicationDate: bookResponse?.desiredReleaseDate || '',
|
||||
desiredWordCount: bookResponse?.desiredWordCount || 0,
|
||||
totalWordCount: 0,
|
||||
coverImage: bookResponse?.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center h-full overflow-hidden w-full text-text-primary font-['Lora']">
|
||||
{session?.user && (
|
||||
<div data-guide="search-bar" className="w-full max-w-3xl px-4 pt-6 pb-4">
|
||||
<SearchBook searchQuery={searchQuery} setSearchQuery={setSearchQuery}/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col w-full overflow-y-auto h-full min-h-0 flex-grow">
|
||||
{
|
||||
isLoadingBooks ? (
|
||||
<>
|
||||
<div className="text-center mb-8 px-6">
|
||||
<h1 className="font-['ADLaM_Display'] text-4xl mb-3 text-text-primary">{t("bookList.library")}</h1>
|
||||
<p className="text-muted italic text-lg">{t("bookList.booksAreMirrors")}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full mb-10">
|
||||
<div className="flex justify-between items-center w-full max-w-5xl mx-auto mb-4 px-6">
|
||||
<div className="h-8 bg-secondary/30 rounded-xl w-32 animate-pulse"></div>
|
||||
<div className="h-6 bg-secondary/20 rounded-lg w-24 animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-start w-full px-4">
|
||||
{Array.from({length: 6}).map((_, id: number) => (
|
||||
<div key={id}
|
||||
className="w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border">
|
||||
<BookCardSkeleton/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : Object.entries(filteredGroupedBooks).length > 0 ? (
|
||||
<>
|
||||
<div className="text-center mb-8 px-6">
|
||||
<h1 className="font-['ADLaM_Display'] text-4xl mb-3 text-text-primary">{t("bookList.library")}</h1>
|
||||
<p className="text-muted italic text-lg">{t("bookList.booksAreMirrors")}</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(filteredGroupedBooks).map(([category, books], index) => (
|
||||
<div {...(index === 0 && {'data-guide': 'book-category'})} key={category}
|
||||
className="w-full mb-10">
|
||||
<div
|
||||
className="flex justify-between items-center w-full max-w-5xl mx-auto mb-6 px-6">
|
||||
<h2 className="text-3xl text-text-primary capitalize font-['ADLaM_Display'] flex items-center gap-3">
|
||||
<span className="w-1 h-8 bg-primary rounded-full"></span>
|
||||
{category}
|
||||
</h2>
|
||||
<span
|
||||
className="text-muted text-lg font-medium bg-secondary/30 px-4 py-1.5 rounded-full">{books.length} {t("bookList.works")}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-start w-full px-4">
|
||||
{
|
||||
books.map((book: BookProps, idx) => (
|
||||
<div key={book.bookId}
|
||||
{...(idx === 0 && {'data-guide': 'book-card'})}
|
||||
className={`w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
|
||||
<BookCard book={book}
|
||||
onClickCallback={getBook}
|
||||
index={idx}/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center p-8 max-w-lg">
|
||||
<div
|
||||
className="w-24 h-24 bg-primary/20 text-primary rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg animate-pulse">
|
||||
<FontAwesomeIcon icon={faBook} className={'w-12 h-12'}/>
|
||||
</div>
|
||||
<h2 className="text-4xl font-['ADLaM_Display'] mb-4 text-text-primary">{t("bookList.welcomeWritingWorkshop")}</h2>
|
||||
<p className="text-muted mb-6 text-lg leading-relaxed">
|
||||
{t("bookList.whitePageText")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
bookGuide && <GuideTour stepId={0} steps={bookGuideSteps} onComplete={handleFirstBookGuide}
|
||||
onClose={() => setBookGuide(false)}/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
components/book/SearchBook.tsx
Normal file
33
components/book/SearchBook.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {faSearch} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import React, {ChangeEvent, Dispatch, SetStateAction} from "react";
|
||||
import {t} from "i18next";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
|
||||
export default function SearchBook(
|
||||
{
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
setSearchQuery: Dispatch<SetStateAction<string>>
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className="flex items-center relative my-5 w-full max-w-3xl">
|
||||
<div className="relative flex-grow">
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon icon={faSearch}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 text-primary w-5 h-5 pointer-events-none z-10"/>
|
||||
<div className="pl-11">
|
||||
<TextInput
|
||||
value={searchQuery}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
|
||||
placeholder={t("searchBook.placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
232
components/book/settings/BasicInformationSetting.tsx
Normal file
232
components/book/settings/BasicInformationSetting.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client'
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFeather, faTimes} from "@fortawesome/free-solid-svg-icons";
|
||||
import {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import NumberInput from "@/components/form/NumberInput";
|
||||
import DatePicker from "@/components/form/DatePicker";
|
||||
import {configs} from "@/lib/configs";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {BookProps} from "@/lib/models/Book";
|
||||
|
||||
function BasicInformationSetting(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book, setBook} = useContext(BookContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
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 () {
|
||||
return {
|
||||
handleSave: handleSave
|
||||
};
|
||||
});
|
||||
|
||||
async function handleCoverImageChange(e: ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||
const file: File | undefined = e.target.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
errorMessage(t('basicInformationSetting.error.noFileSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('bookId', bookId);
|
||||
formData.append('picture', file);
|
||||
|
||||
try {
|
||||
const query: AxiosResponse<ArrayBuffer> = await axios({
|
||||
method: "POST",
|
||||
url: configs.apiUrl + `book/cover?bookid=${bookId}`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userToken}`,
|
||||
},
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: 'web',
|
||||
},
|
||||
data: formData,
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
|
||||
const contentType: string = query.headers['content-type'] || 'image/jpeg';
|
||||
const blob = new Blob([query.data], {type: contentType});
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = function (): void {
|
||||
if (typeof reader.result === 'string') {
|
||||
setCurrentImage(reader.result);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
} catch (e: unknown) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
|
||||
throw new Error(serverMessage as string);
|
||||
} else if (e instanceof Error) {
|
||||
throw new Error(e.message);
|
||||
} else {
|
||||
throw new Error('An unexpected error occurred');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveCurrentImage(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<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 {
|
||||
const response: boolean = await System.authPostToServer<boolean>('book/basic-information', {
|
||||
title: title,
|
||||
subTitle: subTitle,
|
||||
summary: summary,
|
||||
publicationDate: publicationDate,
|
||||
wordCount: wordCount,
|
||||
bookId: bookId
|
||||
}, userToken, lang);
|
||||
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="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<InputField fieldName={t('basicInformationSetting.fields.title')} input={<TextInput
|
||||
value={title}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
|
||||
placeholder={t('basicInformationSetting.fields.titlePlaceholder')}
|
||||
/>}/>
|
||||
<InputField fieldName={t('basicInformationSetting.fields.subtitle')} input={<TextInput
|
||||
value={subTitle}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setSubTitle(e.target.value)}
|
||||
placeholder={t('basicInformationSetting.fields.subtitlePlaceholder')}
|
||||
/>}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t('basicInformationSetting.fields.summary')} input={<TexteAreaInput
|
||||
value={summary}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setSummary(e.target.value)}
|
||||
placeholder={t('basicInformationSetting.fields.summaryPlaceholder')}
|
||||
/>}/>
|
||||
</div>
|
||||
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<InputField fieldName={t('basicInformationSetting.fields.publicationDate')} input={
|
||||
<DatePicker
|
||||
date={publicationDate}
|
||||
setDate={(e: ChangeEvent<HTMLInputElement>) => setPublicationDate(e.target.value)}
|
||||
/>
|
||||
}/>
|
||||
<InputField fieldName={t('basicInformationSetting.fields.wordCount')} input={
|
||||
<NumberInput value={wordCount} setValue={setWordCount}
|
||||
placeholder={t('basicInformationSetting.fields.wordCountPlaceholder')}/>
|
||||
}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
{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 shadow-md w-full h-auto"/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute -top-2 -right-2 bg-error/90 hover:bg-error text-white rounded-full w-8 h-8 flex items-center justify-center hover:scale-110 transition-all duration-200 shadow-lg"
|
||||
onClick={handleRemoveCurrentImage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-lg">
|
||||
<div
|
||||
className="p-6 border-2 border-dashed border-secondary/50 rounded-xl bg-secondary/20 hover:border-primary/60 hover:bg-secondary/30 transition-all duration-200 shadow-inner">
|
||||
<InputField fieldName={t('basicInformationSetting.fields.coverImage')}
|
||||
actionIcon={faFeather}
|
||||
actionLabel={t('basicInformationSetting.fields.generateWithQuillSense')}
|
||||
action={async () => {
|
||||
}} input={<input
|
||||
type="file"
|
||||
id="coverImage"
|
||||
accept="image/png, image/jpeg"
|
||||
onChange={handleCoverImageChange}
|
||||
className="w-full text-text-secondary focus:outline-none file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-background hover:file:bg-primary-dark file:cursor-pointer"
|
||||
/>}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(BasicInformationSetting);
|
||||
18
components/book/settings/BookSetting.tsx
Normal file
18
components/book/settings/BookSetting.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import {useState} from "react";
|
||||
import BookSettingSidebar from "@/components/book/settings/BookSettingSidebar";
|
||||
import BookSettingOption from "@/components/book/settings/BookSettingOption";
|
||||
|
||||
export default function BookSetting() {
|
||||
const [currentSetting, setCurrentSetting] = useState<string>('basic-information')
|
||||
return (
|
||||
<div
|
||||
className={'flex justify-start bg-tertiary/90 backdrop-blur-sm rounded-2xl overflow-hidden border border-secondary/50 shadow-2xl'}>
|
||||
<div className={'bg-secondary/30 backdrop-blur-sm w-1/4 border-r border-secondary/50'}>
|
||||
<BookSettingSidebar selectedSetting={currentSetting} setSelectedSetting={setCurrentSetting}/>
|
||||
</div>
|
||||
<div className={'flex-1 setting-container bg-tertiary/50 p-6'}>
|
||||
<BookSettingOption setting={currentSetting}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
components/book/settings/BookSettingOption.tsx
Normal file
118
components/book/settings/BookSettingOption.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import BasicInformationSetting from "./BasicInformationSetting";
|
||||
import GuideLineSetting from "./guide-line/GuideLineSetting";
|
||||
import StorySetting from "./story/StorySetting";
|
||||
import WorldSetting from "@/components/book/settings/world/WorldSetting";
|
||||
import {faPen, faSave} from "@fortawesome/free-solid-svg-icons";
|
||||
import {RefObject, useRef} from "react";
|
||||
import PanelHeader from "@/components/PanelHeader";
|
||||
import LocationComponent from "@/components/book/settings/locations/LocationComponent";
|
||||
import CharacterComponent from "@/components/book/settings/characters/CharacterComponent";
|
||||
import {useTranslations} from "next-intl"; // Ajouté pour la traduction
|
||||
|
||||
export default function BookSettingOption(
|
||||
{
|
||||
setting,
|
||||
}: {
|
||||
setting: string;
|
||||
}) {
|
||||
const t = useTranslations(); // Ajouté pour la traduction
|
||||
|
||||
const basicInfoRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
const guideLineRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
const storyRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
const worldRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
const locationRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
const characterRef: RefObject<{ handleSave: () => Promise<void> } | null> = useRef<{
|
||||
handleSave: () => Promise<void>
|
||||
}>(null);
|
||||
|
||||
function renderTitle(): string {
|
||||
switch (setting) {
|
||||
case 'basic-information':
|
||||
return t("bookSettingOption.basicInformation");
|
||||
case 'guide-line':
|
||||
return t("bookSettingOption.guideLine");
|
||||
case 'story':
|
||||
return t("bookSettingOption.storyPlan");
|
||||
case 'world':
|
||||
return t("bookSettingOption.manageWorlds");
|
||||
case 'locations':
|
||||
return t("bookSettingOption.yourLocations");
|
||||
case 'characters':
|
||||
return t("bookSettingOption.characters");
|
||||
case 'objects':
|
||||
return t("bookSettingOption.objectsList");
|
||||
case 'goals':
|
||||
return t("bookSettingOption.bookGoals");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveClick(): Promise<void> {
|
||||
switch (setting) {
|
||||
case 'basic-information':
|
||||
basicInfoRef.current?.handleSave();
|
||||
break;
|
||||
case 'guide-line':
|
||||
guideLineRef.current?.handleSave();
|
||||
break;
|
||||
case 'story':
|
||||
storyRef.current?.handleSave();
|
||||
break;
|
||||
case 'world':
|
||||
worldRef.current?.handleSave();
|
||||
break;
|
||||
case 'locations':
|
||||
locationRef.current?.handleSave();
|
||||
break;
|
||||
case 'characters':
|
||||
characterRef.current?.handleSave();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PanelHeader
|
||||
icon={faPen}
|
||||
badge={`BI`}
|
||||
title={renderTitle()}
|
||||
description={``}
|
||||
secondActionCallback={handleSaveClick}
|
||||
callBackAction={handleSaveClick}
|
||||
secondActionIcon={faSave}
|
||||
/>
|
||||
<div className="bg-secondary/10 rounded-xl overflow-auto max-h-[calc(100vh-250px)] p-1">
|
||||
{
|
||||
setting === 'basic-information' ? (
|
||||
<BasicInformationSetting ref={basicInfoRef}/>
|
||||
) : setting === 'guide-line' ? (
|
||||
<GuideLineSetting ref={guideLineRef}/>
|
||||
) : setting === 'story' ? (
|
||||
<StorySetting ref={storyRef}/>
|
||||
) : setting === 'world' ? (
|
||||
<WorldSetting ref={worldRef}/>
|
||||
) : setting === 'locations' ? (
|
||||
<LocationComponent ref={locationRef}/>
|
||||
) : setting === 'characters' ? (
|
||||
<CharacterComponent ref={characterRef}/>
|
||||
) : <div
|
||||
className="text-text-secondary py-4 text-center">{t("bookSettingOption.notAvailable")}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
components/book/settings/BookSettingSidebar.tsx
Normal file
91
components/book/settings/BookSettingSidebar.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import Link from "next/link";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBook, faGlobe, faListAlt, faMapMarkedAlt, faPencilAlt, faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import React, {Dispatch, SetStateAction} from "react";
|
||||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||
import {useTranslations} from "next-intl";
|
||||
|
||||
interface BookSettingOption {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconDefinition;
|
||||
}
|
||||
|
||||
export default function BookSettingSidebar(
|
||||
{
|
||||
selectedSetting,
|
||||
setSelectedSetting
|
||||
}: {
|
||||
selectedSetting: string,
|
||||
setSelectedSetting: Dispatch<SetStateAction<string>>
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
|
||||
const settings: BookSettingOption[] = [
|
||||
{
|
||||
id: 'basic-information',
|
||||
name: 'bookSetting.basicInformation',
|
||||
icon: faPencilAlt
|
||||
},
|
||||
{
|
||||
id: 'guide-line',
|
||||
name: 'bookSetting.guideLine',
|
||||
icon: faListAlt
|
||||
},
|
||||
{
|
||||
id: 'story',
|
||||
name: 'bookSetting.story',
|
||||
icon: faBook
|
||||
},
|
||||
{
|
||||
id: 'world',
|
||||
name: 'bookSetting.world',
|
||||
icon: faGlobe
|
||||
},
|
||||
{
|
||||
id: 'locations',
|
||||
name: 'bookSetting.locations',
|
||||
icon: faMapMarkedAlt
|
||||
},
|
||||
{
|
||||
id: 'characters',
|
||||
name: 'bookSetting.characters',
|
||||
icon: faUser
|
||||
},
|
||||
// {
|
||||
// id: 'objects',
|
||||
// name: t('bookSetting.objects'),
|
||||
// icon: faLocationArrow
|
||||
// },
|
||||
// {
|
||||
// id: 'goals',
|
||||
// name: t('bookSetting.goals'),
|
||||
// icon: faCogs
|
||||
// },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="py-6 px-3">
|
||||
<nav className="space-y-1">
|
||||
{
|
||||
settings.map((setting: BookSettingOption) => (
|
||||
<Link
|
||||
key={setting.id}
|
||||
href={''}
|
||||
onClick={(): void => setSelectedSetting(setting.id)}
|
||||
className={`flex items-center text-base rounded-xl transition-all duration-200 ${
|
||||
selectedSetting === setting.id
|
||||
? 'bg-primary/20 text-text-primary border-l-4 border-primary font-semibold shadow-md scale-105'
|
||||
: 'text-text-secondary hover:bg-secondary/50 hover:text-text-primary hover:scale-102'
|
||||
} p-3 mb-1`}>
|
||||
<FontAwesomeIcon icon={setting.icon}
|
||||
className={`mr-3 ${selectedSetting === setting.id ? 'text-primary w-5 h-5' : 'text-text-secondary w-5 h-5'}`}/>
|
||||
{t(setting.name)}
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
90
components/book/settings/DeleteBook.tsx
Normal file
90
components/book/settings/DeleteBook.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import React, {useContext, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {BookProps} from "@/lib/models/Book";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import AlertBox from "@/components/AlertBox";
|
||||
|
||||
interface DeleteBookProps {
|
||||
bookId: string;
|
||||
}
|
||||
|
||||
export default function DeleteBook({bookId}: DeleteBookProps) {
|
||||
const {session, setSession} = useContext(SessionContext);
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
const [showConfirmBox, setShowConfirmBox] = useState<boolean>(false);
|
||||
const {errorMessage} = useContext<AlertContextProps>(AlertContext)
|
||||
|
||||
function handleConfirmation(): void {
|
||||
setShowConfirmBox(true);
|
||||
}
|
||||
|
||||
async function handleDeleteBook(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(
|
||||
`book/delete`,
|
||||
{
|
||||
id: bookId,
|
||||
},
|
||||
session.accessToken,
|
||||
lang
|
||||
);
|
||||
if (response) {
|
||||
setShowConfirmBox(false);
|
||||
const updatedBooks: BookProps[] = (session.user?.books || []).reduce((acc: BookProps[], book: BookProps): BookProps[] => {
|
||||
if (book.bookId !== bookId) {
|
||||
acc.push({...book});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
if (!response) {
|
||||
errorMessage("Une erreur est survenue lors de la suppression du livre.");
|
||||
return;
|
||||
}
|
||||
const updatedUser = {
|
||||
...(JSON.parse(JSON.stringify(session.user))),
|
||||
books: updatedBooks
|
||||
};
|
||||
|
||||
const newSession = {
|
||||
...JSON.parse(JSON.stringify(session)),
|
||||
user: updatedUser,
|
||||
isConnected: true,
|
||||
accessToken: session.accessToken
|
||||
};
|
||||
|
||||
setSession(newSession);
|
||||
|
||||
setTimeout((): void => {
|
||||
setSession({...newSession});
|
||||
}, 0);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message)
|
||||
} else {
|
||||
errorMessage("Une erreur inconnue est survenue lors de la suppression du livre.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleConfirmation}
|
||||
className="text-muted hover:text-error hover:bg-error/10 transition-all duration-200 p-2 rounded-lg hover:scale-110">
|
||||
<FontAwesomeIcon icon={faTrash} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
{
|
||||
showConfirmBox && (
|
||||
<AlertBox title={'Suppression du livre'}
|
||||
message={'Vous être sur le point de supprimer votre livre définitivement.'} type={"danger"}
|
||||
onConfirm={handleDeleteBook} onCancel={() => setShowConfirmBox(false)}
|
||||
confirmText={'Supprimer'} cancelText={'Annuler'}/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
264
components/book/settings/characters/CharacterComponent.tsx
Normal file
264
components/book/settings/characters/CharacterComponent.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
'use client';
|
||||
import React, {Dispatch, forwardRef, SetStateAction, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {Attribute, CharacterProps} from "@/lib/models/Character";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import CharacterList from './CharacterList';
|
||||
import System from '@/lib/models/System';
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import CharacterDetail from "@/components/book/settings/characters/CharacterDetail";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface CharacterDetailProps {
|
||||
selectedCharacter: CharacterProps | null;
|
||||
setSelectedCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
|
||||
handleCharacterChange: (key: keyof CharacterProps, value: string) => void;
|
||||
handleAddElement: (section: keyof CharacterProps, element: any) => void;
|
||||
handleRemoveElement: (
|
||||
section: keyof CharacterProps,
|
||||
index: number,
|
||||
attrId: string,
|
||||
) => void;
|
||||
handleSaveCharacter: () => void;
|
||||
}
|
||||
|
||||
const initialCharacterState: CharacterProps = {
|
||||
id: null,
|
||||
name: '',
|
||||
lastName: '',
|
||||
category: 'none',
|
||||
title: '',
|
||||
role: '',
|
||||
image: 'https://via.placeholder.com/150',
|
||||
biography: '',
|
||||
history: '',
|
||||
physical: [],
|
||||
psychological: [],
|
||||
relations: [],
|
||||
skills: [],
|
||||
weaknesses: [],
|
||||
strengths: [],
|
||||
goals: [],
|
||||
motivations: [],
|
||||
};
|
||||
|
||||
export function CharacterComponent(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const [characters, setCharacters] = useState<CharacterProps[]>([]);
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<CharacterProps | null>(null);
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleSaveCharacter,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getCharacters().then();
|
||||
}, []);
|
||||
|
||||
async function getCharacters(): Promise<void> {
|
||||
try {
|
||||
const response: CharacterProps[] = await System.authGetQueryToServer<CharacterProps[]>(`character/list`, session.accessToken, lang, {
|
||||
bookid: book?.bookId,
|
||||
});
|
||||
if (response) {
|
||||
setCharacters(response);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCharacterClick(character: CharacterProps): void {
|
||||
setSelectedCharacter({...character});
|
||||
}
|
||||
|
||||
function handleAddCharacter(): void {
|
||||
setSelectedCharacter({...initialCharacterState});
|
||||
}
|
||||
|
||||
async function handleSaveCharacter(): Promise<void> {
|
||||
if (selectedCharacter) {
|
||||
const updatedCharacter: CharacterProps = {...selectedCharacter};
|
||||
if (selectedCharacter.id === null) {
|
||||
await addNewCharacter(updatedCharacter);
|
||||
} else {
|
||||
await updateCharacter(updatedCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewCharacter(updatedCharacter: CharacterProps): Promise<void> {
|
||||
if (!updatedCharacter.name) {
|
||||
errorMessage(t("characterComponent.errorNameRequired"));
|
||||
return;
|
||||
}
|
||||
if (updatedCharacter.category === 'none') {
|
||||
errorMessage(t("characterComponent.errorCategoryRequired"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const characterId: string = await System.authPostToServer<string>(`character/add`, {
|
||||
bookId: book?.bookId,
|
||||
character: updatedCharacter,
|
||||
}, session.accessToken, lang);
|
||||
if (!characterId) {
|
||||
errorMessage(t("characterComponent.errorAddCharacter"));
|
||||
return;
|
||||
}
|
||||
updatedCharacter.id = characterId;
|
||||
setCharacters([...characters, updatedCharacter]);
|
||||
setSelectedCharacter(null);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCharacter(updatedCharacter: CharacterProps,): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPostToServer<boolean>(`character/update`, {
|
||||
character: updatedCharacter,
|
||||
}, session.accessToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("characterComponent.errorUpdateCharacter"));
|
||||
return;
|
||||
}
|
||||
setCharacters(
|
||||
characters.map((char: CharacterProps): CharacterProps =>
|
||||
char.id === updatedCharacter.id ? updatedCharacter : char,
|
||||
),
|
||||
);
|
||||
setSelectedCharacter(null);
|
||||
successMessage(t("characterComponent.successUpdate"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCharacterChange(
|
||||
key: keyof CharacterProps,
|
||||
value: string,
|
||||
): void {
|
||||
if (selectedCharacter) {
|
||||
setSelectedCharacter({...selectedCharacter, [key]: value});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddElement(
|
||||
section: keyof CharacterProps,
|
||||
value: Attribute,
|
||||
): Promise<void> {
|
||||
if (selectedCharacter) {
|
||||
if (selectedCharacter.id === null) {
|
||||
const updatedSection: any[] = [
|
||||
...(selectedCharacter[section] as any[]),
|
||||
value,
|
||||
];
|
||||
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
|
||||
} else {
|
||||
try {
|
||||
const attributeId: string = await System.authPostToServer<string>(`character/attribute/add`, {
|
||||
characterId: selectedCharacter.id,
|
||||
type: section,
|
||||
name: value.name,
|
||||
}, session.accessToken, lang);
|
||||
if (!attributeId) {
|
||||
errorMessage(t("characterComponent.errorAddAttribute"));
|
||||
return;
|
||||
}
|
||||
const newValue: Attribute = {
|
||||
name: value.name,
|
||||
id: attributeId,
|
||||
};
|
||||
const updatedSection: Attribute[] = [...(selectedCharacter[section] as Attribute[]), newValue,];
|
||||
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveElement(
|
||||
section: keyof CharacterProps,
|
||||
index: number,
|
||||
attrId: string,
|
||||
): Promise<void> {
|
||||
if (selectedCharacter) {
|
||||
if (selectedCharacter.id === null) {
|
||||
const updatedSection: Attribute[] = (
|
||||
selectedCharacter[section] as Attribute[]
|
||||
).filter((_, i: number): boolean => i !== index);
|
||||
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
|
||||
} else {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(`character/attribute/delete`, {
|
||||
attributeId: attrId,
|
||||
}, session.accessToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("characterComponent.errorRemoveAttribute"));
|
||||
return;
|
||||
}
|
||||
const updatedSection: Attribute[] = (
|
||||
selectedCharacter[section] as Attribute[]
|
||||
).filter((_, i: number): boolean => i !== index);
|
||||
setSelectedCharacter({
|
||||
...selectedCharacter,
|
||||
[section]: updatedSection,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{selectedCharacter ? (
|
||||
<CharacterDetail
|
||||
selectedCharacter={selectedCharacter}
|
||||
setSelectedCharacter={setSelectedCharacter}
|
||||
handleAddElement={handleAddElement}
|
||||
handleRemoveElement={handleRemoveElement}
|
||||
handleCharacterChange={handleCharacterChange}
|
||||
handleSaveCharacter={handleSaveCharacter}
|
||||
/>
|
||||
) : (
|
||||
<CharacterList
|
||||
characters={characters}
|
||||
handleAddCharacter={handleAddCharacter}
|
||||
handleCharacterClick={handleCharacterClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(CharacterComponent);
|
||||
230
components/book/settings/characters/CharacterDetail.tsx
Normal file
230
components/book/settings/characters/CharacterDetail.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {
|
||||
CharacterAttribute,
|
||||
characterCategories,
|
||||
CharacterElement,
|
||||
characterElementCategory,
|
||||
CharacterProps,
|
||||
characterTitle
|
||||
} from "@/lib/models/Character";
|
||||
import System from "@/lib/models/System";
|
||||
import {
|
||||
faAddressCard,
|
||||
faArrowLeft,
|
||||
faBook,
|
||||
faLayerGroup,
|
||||
faPlus,
|
||||
faSave,
|
||||
faScroll,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {Dispatch, SetStateAction, useContext, useEffect} from "react";
|
||||
import CharacterSectionElement from "@/components/book/settings/characters/CharacterSectionElement";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext} from "@/context/LangContext";
|
||||
|
||||
interface CharacterDetailProps {
|
||||
selectedCharacter: CharacterProps | null;
|
||||
setSelectedCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
|
||||
handleCharacterChange: (key: keyof CharacterProps, value: string) => void;
|
||||
handleAddElement: (section: keyof CharacterProps, element: any) => void;
|
||||
handleRemoveElement: (
|
||||
section: keyof CharacterProps,
|
||||
index: number,
|
||||
attrId: string,
|
||||
) => void;
|
||||
handleSaveCharacter: () => void;
|
||||
}
|
||||
|
||||
export default function CharacterDetail(
|
||||
{
|
||||
setSelectedCharacter,
|
||||
selectedCharacter,
|
||||
handleCharacterChange,
|
||||
handleRemoveElement,
|
||||
handleAddElement,
|
||||
handleSaveCharacter,
|
||||
}: CharacterDetailProps
|
||||
) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext(LangContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
|
||||
useEffect((): void => {
|
||||
if (selectedCharacter?.id !== null) {
|
||||
getAttributes().then();
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
const response: CharacterAttribute = await System.authGetQueryToServer<CharacterAttribute>(`character/attribute`, session.accessToken, lang, {
|
||||
characterId: selectedCharacter?.id,
|
||||
});
|
||||
if (response) {
|
||||
setSelectedCharacter({
|
||||
id: selectedCharacter?.id ?? '',
|
||||
name: selectedCharacter?.name ?? '',
|
||||
image: selectedCharacter?.image ?? '',
|
||||
lastName: selectedCharacter?.lastName ?? '',
|
||||
category: selectedCharacter?.category ?? 'none',
|
||||
title: selectedCharacter?.title ?? '',
|
||||
biography: selectedCharacter?.biography,
|
||||
history: selectedCharacter?.history,
|
||||
role: selectedCharacter?.role ?? '',
|
||||
physical: response.physical ?? [],
|
||||
psychological: response.psychological ?? [],
|
||||
relations: response.relations ?? [],
|
||||
skills: response.skills ?? [],
|
||||
weaknesses: response.weaknesses ?? [],
|
||||
strengths: response.strengths ?? [],
|
||||
goals: response.goals ?? [],
|
||||
motivations: response.motivations ?? [],
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("characterDetail.fetchAttributesError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="flex justify-between items-center p-4 border-b border-secondary/50 bg-tertiary/50 backdrop-blur-sm">
|
||||
<button onClick={() => setSelectedCharacter(null)}
|
||||
className="flex items-center gap-2 bg-secondary/50 py-2 px-4 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="text-primary w-4 h-4"/>
|
||||
<span className="text-text-primary font-medium">{t("characterDetail.back")}</span>
|
||||
</button>
|
||||
<span className="text-text-primary font-semibold text-lg">
|
||||
{selectedCharacter?.name || t("characterDetail.newCharacter")}
|
||||
</span>
|
||||
<button onClick={handleSaveCharacter}
|
||||
className="flex items-center justify-center bg-primary w-10 h-10 rounded-xl border border-primary-dark shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200">
|
||||
<FontAwesomeIcon icon={selectedCharacter?.id ? faSave : faPlus}
|
||||
className="text-text-primary w-5 h-5"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4">
|
||||
<CollapsableArea title={t("characterDetail.basicInfo")} icon={faUser}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<InputField
|
||||
fieldName={t("characterDetail.name")}
|
||||
input={
|
||||
<TextInput
|
||||
value={selectedCharacter?.name || ''}
|
||||
setValue={(e) => handleCharacterChange('name', e.target.value)}
|
||||
placeholder={t("characterDetail.namePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("characterDetail.lastName")}
|
||||
input={
|
||||
<TextInput
|
||||
value={selectedCharacter?.lastName || ''}
|
||||
setValue={(e) => handleCharacterChange('lastName', e.target.value)}
|
||||
placeholder={t("characterDetail.lastNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("characterDetail.role")}
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={selectedCharacter?.category || 'none'}
|
||||
onChangeCallBack={(e) => setSelectedCharacter(prev =>
|
||||
prev ? {...prev, category: e.target.value as CharacterProps['category']} : prev
|
||||
)}
|
||||
data={characterCategories}
|
||||
/>
|
||||
}
|
||||
icon={faLayerGroup}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("characterDetail.title")}
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={selectedCharacter?.title || 'none'}
|
||||
onChangeCallBack={(e) => handleCharacterChange('title', e.target.value)}
|
||||
data={characterTitle}
|
||||
/>
|
||||
}
|
||||
icon={faAddressCard}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("characterDetail.historySection")} icon={faUser}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<InputField
|
||||
fieldName={t("characterDetail.biography")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={selectedCharacter?.biography || ''}
|
||||
setValue={(e) => handleCharacterChange('biography', e.target.value)}
|
||||
placeholder={t("characterDetail.biographyPlaceholder")}
|
||||
/>
|
||||
}
|
||||
icon={faBook}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("characterDetail.history")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={selectedCharacter?.history || ''}
|
||||
setValue={(e) => handleCharacterChange('history', e.target.value)}
|
||||
placeholder={t("characterDetail.historyPlaceholder")}
|
||||
/>
|
||||
}
|
||||
icon={faScroll}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("characterDetail.roleFull")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={selectedCharacter?.role || ''}
|
||||
setValue={(e) => handleCharacterChange('role', e.target.value)}
|
||||
placeholder={t("characterDetail.roleFullPlaceholder")}
|
||||
/>
|
||||
}
|
||||
icon={faScroll}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
{characterElementCategory.map((item: CharacterElement, index: number) => (
|
||||
<CharacterSectionElement
|
||||
key={index}
|
||||
title={item.title}
|
||||
section={item.section}
|
||||
placeholder={item.placeholder}
|
||||
icon={item.icon}
|
||||
selectedCharacter={selectedCharacter as CharacterProps}
|
||||
setSelectedCharacter={setSelectedCharacter}
|
||||
handleAddElement={handleAddElement}
|
||||
handleRemoveElement={handleRemoveElement}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
components/book/settings/characters/CharacterList.tsx
Normal file
124
components/book/settings/characters/CharacterList.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import {characterCategories, CharacterProps} from "@/lib/models/Character";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import {faChevronRight, faPlus, faUser} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {useState} from "react";
|
||||
import {useTranslations} from "next-intl";
|
||||
|
||||
interface CharacterListProps {
|
||||
characters: CharacterProps[];
|
||||
handleCharacterClick: (character: CharacterProps) => void;
|
||||
handleAddCharacter: () => void;
|
||||
}
|
||||
|
||||
export default function CharacterList(
|
||||
{
|
||||
characters,
|
||||
handleCharacterClick,
|
||||
handleAddCharacter,
|
||||
}: CharacterListProps) {
|
||||
const t = useTranslations();
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
function getFilteredCharacters(
|
||||
characters: CharacterProps[],
|
||||
searchQuery: string,
|
||||
): CharacterProps[] {
|
||||
return characters.filter(
|
||||
(char: CharacterProps) =>
|
||||
char.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(char.lastName &&
|
||||
char.lastName.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
);
|
||||
}
|
||||
|
||||
const filteredCharacters: CharacterProps[] = getFilteredCharacters(
|
||||
characters,
|
||||
searchQuery,
|
||||
);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="px-4 mb-4">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={searchQuery}
|
||||
setValue={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t("characterList.search")}
|
||||
/>
|
||||
}
|
||||
actionIcon={faPlus}
|
||||
actionLabel={t("characterList.add")}
|
||||
addButtonCallBack={async () => handleAddCharacter()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-350px)] px-2">
|
||||
{characterCategories.map((category: SelectBoxProps) => {
|
||||
const categoryCharacters = filteredCharacters.filter(
|
||||
(char: CharacterProps) => char.category === category.value
|
||||
);
|
||||
|
||||
if (categoryCharacters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableArea
|
||||
key={category.value}
|
||||
title={category.label}
|
||||
icon={faUser}
|
||||
children={<div className="space-y-2 p-2">
|
||||
{categoryCharacters.map(char => (
|
||||
<div
|
||||
key={char.id}
|
||||
onClick={() => handleCharacterClick(char)}
|
||||
className="group flex items-center p-4 bg-secondary/30 rounded-xl border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md hover:scale-102 transition-all duration-200 hover:border-primary/50"
|
||||
>
|
||||
<div
|
||||
className="w-14 h-14 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-md group-hover:scale-110 transition-transform">
|
||||
{char.image ? (
|
||||
<img
|
||||
src={char.image}
|
||||
alt={char.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center bg-primary/10 text-primary font-bold text-lg">
|
||||
{char.name?.charAt(0)?.toUpperCase() || '?'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-4 flex-1">
|
||||
<div
|
||||
className="text-text-primary font-bold text-base group-hover:text-primary transition-colors">{char.name || t("characterList.unknown")}</div>
|
||||
<div
|
||||
className="text-text-secondary text-sm mt-0.5">{char.lastName || t("characterList.noLastName")}</div>
|
||||
</div>
|
||||
|
||||
<div className="w-28 px-3">
|
||||
<div
|
||||
className="text-primary text-sm font-semibold truncate">{char.title || t("characterList.noTitle")}</div>
|
||||
<div
|
||||
className="text-muted text-xs truncate mt-0.5">{char.role || t("characterList.noRole")}</div>
|
||||
</div>
|
||||
|
||||
<div className="w-8 flex justify-center">
|
||||
<FontAwesomeIcon icon={faChevronRight}
|
||||
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-4 h-4"/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {useState} from "react";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import {Attribute, CharacterProps} from "@/lib/models/Character";
|
||||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||
import {useTranslations} from "next-intl";
|
||||
|
||||
interface CharacterSectionElementProps {
|
||||
title: string;
|
||||
section: keyof CharacterProps;
|
||||
placeholder: string;
|
||||
icon: IconDefinition;
|
||||
selectedCharacter: CharacterProps;
|
||||
setSelectedCharacter: (character: CharacterProps) => void;
|
||||
handleAddElement: (section: keyof CharacterProps, element: Attribute) => void;
|
||||
handleRemoveElement: (
|
||||
section: keyof CharacterProps,
|
||||
index: number,
|
||||
attrId: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default function CharacterSectionElement(
|
||||
{
|
||||
title,
|
||||
section,
|
||||
placeholder,
|
||||
icon,
|
||||
selectedCharacter,
|
||||
setSelectedCharacter,
|
||||
handleAddElement,
|
||||
handleRemoveElement,
|
||||
}: CharacterSectionElementProps) {
|
||||
const t = useTranslations();
|
||||
const [element, setElement] = useState<string>('');
|
||||
|
||||
function handleAddNewElement() {
|
||||
handleAddElement(section, {id: '', name: element});
|
||||
setElement('');
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableArea title={title} icon={icon}>
|
||||
<div className="space-y-3 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
{Array.isArray(selectedCharacter?.[section]) &&
|
||||
selectedCharacter?.[section].map((item, index: number) => (
|
||||
<div key={index}
|
||||
className="flex items-center gap-2 bg-secondary/30 rounded-xl border-l-4 border-primary shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<input
|
||||
className="flex-1 bg-transparent text-text-primary px-3 py-2.5 focus:outline-none placeholder:text-muted/60"
|
||||
value={item.name || item.type || item.description || item.history || ''}
|
||||
onChange={(e) => {
|
||||
const updatedSection = [...(selectedCharacter[section] as any[])];
|
||||
updatedSection[index].name = e.target.value;
|
||||
setSelectedCharacter({
|
||||
...selectedCharacter,
|
||||
[section]: updatedSection,
|
||||
});
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveElement(section, index, item.id)}
|
||||
className="bg-error/90 hover:bg-error w-9 h-9 rounded-full flex items-center justify-center mr-2 shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="text-white w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="border-t border-secondary/50 mt-4 pt-4">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={element}
|
||||
setValue={(e) => setElement(e.target.value)}
|
||||
placeholder={t("characterSectionElement.newItem", {item: title.toLowerCase()})}
|
||||
/>
|
||||
}
|
||||
addButtonCallBack={async () => handleAddNewElement()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
)
|
||||
}
|
||||
181
components/book/settings/goals/page.tsx
Normal file
181
components/book/settings/goals/page.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
'use client'
|
||||
import React, {useState} from 'react';
|
||||
|
||||
interface TimeGoal {
|
||||
desiredReleaseDate: string;
|
||||
maxReleaseDate: string;
|
||||
}
|
||||
|
||||
interface NumbersGoal {
|
||||
minWordsCount: number;
|
||||
maxWordsCount: number;
|
||||
desiredWordsCountByChapter: number;
|
||||
desiredChapterCount: number;
|
||||
}
|
||||
|
||||
interface Goal {
|
||||
id: number;
|
||||
name: string;
|
||||
timeGoal: TimeGoal;
|
||||
numbersGoal: NumbersGoal;
|
||||
}
|
||||
|
||||
export default function GoalsPage() {
|
||||
const [goals, setGoals] = useState<Goal[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'First Goal',
|
||||
timeGoal: {
|
||||
desiredReleaseDate: '',
|
||||
maxReleaseDate: '',
|
||||
},
|
||||
numbersGoal: {
|
||||
minWordsCount: 0,
|
||||
maxWordsCount: 0,
|
||||
desiredWordsCountByChapter: 0,
|
||||
desiredChapterCount: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const [selectedGoalIndex, setSelectedGoalIndex] = useState(0);
|
||||
const [newGoalName, setNewGoalName] = useState('');
|
||||
|
||||
const handleAddGoal = () => {
|
||||
const newGoal: Goal = {
|
||||
id: goals.length + 1,
|
||||
name: newGoalName,
|
||||
timeGoal: {
|
||||
desiredReleaseDate: '',
|
||||
maxReleaseDate: '',
|
||||
},
|
||||
numbersGoal: {
|
||||
minWordsCount: 0,
|
||||
maxWordsCount: 0,
|
||||
desiredWordsCountByChapter: 0,
|
||||
desiredChapterCount: 0,
|
||||
},
|
||||
};
|
||||
setGoals([...goals, newGoal]);
|
||||
setNewGoalName('');
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, field: keyof Goal, subField?: keyof TimeGoal | keyof NumbersGoal) => {
|
||||
const updatedGoals = [...goals];
|
||||
if (subField) {
|
||||
if (field === 'timeGoal' && subField in updatedGoals[selectedGoalIndex].timeGoal) {
|
||||
(updatedGoals[selectedGoalIndex].timeGoal[subField as keyof TimeGoal] as string) = e.target.value;
|
||||
} else if (field === 'numbersGoal' && subField in updatedGoals[selectedGoalIndex].numbersGoal) {
|
||||
(updatedGoals[selectedGoalIndex].numbersGoal[subField as keyof NumbersGoal] as number) = Number(e.target.value);
|
||||
}
|
||||
} else {
|
||||
(updatedGoals[selectedGoalIndex][field] as string) = e.target.value;
|
||||
}
|
||||
setGoals(updatedGoals);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-grow p-8 overflow-y-auto">
|
||||
<section id="goals">
|
||||
<h2 className="text-4xl font-['ADLaM_Display'] text-text-primary mb-6">Goals</h2>
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-6 shadow-lg mb-6">
|
||||
<div className="flex space-x-4 items-center">
|
||||
<select
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
value={selectedGoalIndex}
|
||||
onChange={(e) => setSelectedGoalIndex(parseInt(e.target.value))}
|
||||
>
|
||||
{goals.map((goal, index) => (
|
||||
<option key={goal.id} value={index}>{goal.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={newGoalName}
|
||||
onChange={(e) => setNewGoalName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
placeholder="New Goal Name"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddGoal}
|
||||
className="bg-primary hover:bg-primary-dark text-text-primary font-semibold py-2.5 px-5 rounded-xl shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||
>
|
||||
Add Goal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-3xl font-['ADLaM_Display'] text-text-primary mb-6">{goals[selectedGoalIndex].name}</h2>
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-6 shadow-lg mb-6">
|
||||
<h3 className="text-2xl font-bold text-text-primary mb-4">Time Goal</h3>
|
||||
<label className="block text-text-primary font-medium mb-2" htmlFor="desiredReleaseDate">Desired
|
||||
Release Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="desiredReleaseDate"
|
||||
value={goals[selectedGoalIndex].timeGoal.desiredReleaseDate}
|
||||
onChange={(e) => handleInputChange(e, 'timeGoal', 'desiredReleaseDate')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
<label className="block text-white mb-2 mt-4" htmlFor="maxReleaseDate">Max Release Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="maxReleaseDate"
|
||||
value={goals[selectedGoalIndex].timeGoal.maxReleaseDate}
|
||||
onChange={(e) => handleInputChange(e, 'timeGoal', 'maxReleaseDate')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-6 shadow-lg mb-6">
|
||||
<h3 className="text-2xl font-bold text-text-primary mb-4">Numbers Goal</h3>
|
||||
<label className="block text-text-primary font-medium mb-2" htmlFor="minWordsCount">Min Words
|
||||
Count</label>
|
||||
<input
|
||||
type="number"
|
||||
id="minWordsCount"
|
||||
value={goals[selectedGoalIndex].numbersGoal.minWordsCount}
|
||||
onChange={(e) => handleInputChange(e, 'numbersGoal', 'minWordsCount')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
<label className="block text-white mb-2 mt-4" htmlFor="maxWordsCount">Max Words Count</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxWordsCount"
|
||||
value={goals[selectedGoalIndex].numbersGoal.maxWordsCount}
|
||||
onChange={(e) => handleInputChange(e, 'numbersGoal', 'maxWordsCount')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
<label className="block text-white mb-2 mt-4" htmlFor="desiredWordsCountByChapter">Desired Words
|
||||
Count by Chapter</label>
|
||||
<input
|
||||
type="number"
|
||||
id="desiredWordsCountByChapter"
|
||||
value={goals[selectedGoalIndex].numbersGoal.desiredWordsCountByChapter}
|
||||
onChange={(e) => handleInputChange(e, 'numbersGoal', 'desiredWordsCountByChapter')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
<label className="block text-white mb-2 mt-4" htmlFor="desiredChapterCount">Desired Chapter
|
||||
Count</label>
|
||||
<input
|
||||
type="number"
|
||||
id="desiredChapterCount"
|
||||
value={goals[selectedGoalIndex].numbersGoal.desiredChapterCount}
|
||||
onChange={(e) => handleInputChange(e, 'numbersGoal', 'desiredChapterCount')}
|
||||
className="w-full p-2 rounded-lg bg-gray-800 text-white border-none outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-8">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-primary hover:bg-primary-dark text-text-primary font-semibold py-2.5 px-6 rounded-xl shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
421
components/book/settings/guide-line/GuideLineSetting.tsx
Normal file
421
components/book/settings/guide-line/GuideLineSetting.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
'use client'
|
||||
import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import System from '@/lib/models/System';
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {GuideLine, GuideLineAI} from "@/lib/models/Book";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import {
|
||||
advancedDialogueTypes,
|
||||
advancedNarrativePersons,
|
||||
beginnerDialogueTypes,
|
||||
beginnerNarrativePersons,
|
||||
intermediateDialogueTypes,
|
||||
intermediateNarrativePersons,
|
||||
langues,
|
||||
verbalTime
|
||||
} from "@/lib/models/Story";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext} from "@/context/LangContext";
|
||||
|
||||
function GuideLineSetting(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const userToken: string = session?.accessToken ? session?.accessToken : '';
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const bookId = book?.bookId as string;
|
||||
const [activeTab, setActiveTab] = useState('personal');
|
||||
const authorLevel: string = session.user?.writingLevel?.toString() ?? '1';
|
||||
|
||||
const [tone, setTone] = useState<string>('');
|
||||
const [atmosphere, setAtmosphere] = useState<string>('');
|
||||
const [writingStyle, setWritingStyle] = useState<string>('');
|
||||
const [themes, setThemes] = useState<string>('');
|
||||
const [symbolism, setSymbolism] = useState<string>('');
|
||||
const [motifs, setMotifs] = useState<string>('');
|
||||
const [narrativeVoice, setNarrativeVoice] = useState<string>('');
|
||||
const [pacing, setPacing] = useState<string>('');
|
||||
const [intendedAudience, setIntendedAudience] = useState<string>('');
|
||||
const [keyMessages, setKeyMessages] = useState<string>('');
|
||||
|
||||
const [plotSummary, setPlotSummary] = useState<string>('');
|
||||
const [narrativeType, setNarrativeType] = useState<string>('');
|
||||
const [verbTense, setVerbTense] = useState<string>('');
|
||||
const [dialogueType, setDialogueType] = useState<string>('');
|
||||
const [toneAtmosphere, setToneAtmosphere] = useState<string>('');
|
||||
const [language, setLanguage] = useState<string>('');
|
||||
|
||||
useEffect((): void => {
|
||||
if (activeTab === 'personal') {
|
||||
getGuideLine().then();
|
||||
} else {
|
||||
getAIGuideLine().then();
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
{
|
||||
if (activeTab === 'personal') {
|
||||
return {
|
||||
handleSave: savePersonal
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
handleSave: saveQuillSense
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function getAIGuideLine(): Promise<void> {
|
||||
try {
|
||||
const response: GuideLineAI = await System.authGetQueryToServer<GuideLineAI>(`book/ai/guideline`, userToken, lang, {id: bookId});
|
||||
if (response) {
|
||||
setPlotSummary(response.globalResume);
|
||||
setVerbTense(response.verbeTense?.toString() || '');
|
||||
setNarrativeType(response.narrativeType?.toString() || '');
|
||||
setDialogueType(response.dialogueType?.toString() || '');
|
||||
setToneAtmosphere(response.atmosphere);
|
||||
setLanguage(response.langue?.toString() || '');
|
||||
setThemes(response.themes);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("guideLineSetting.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getGuideLine(): Promise<void> {
|
||||
try {
|
||||
const response: GuideLine =
|
||||
await System.authGetQueryToServer<GuideLine>(
|
||||
`book/guide-line`,
|
||||
userToken,
|
||||
lang,
|
||||
{id: bookId},
|
||||
);
|
||||
if (response) {
|
||||
setTone(response.tone);
|
||||
setAtmosphere(response.atmosphere);
|
||||
setWritingStyle(response.writingStyle);
|
||||
setThemes(response.themes);
|
||||
setSymbolism(response.symbolism);
|
||||
setMotifs(response.motifs);
|
||||
setNarrativeVoice(response.narrativeVoice);
|
||||
setPacing(response.pacing);
|
||||
setIntendedAudience(response.intendedAudience);
|
||||
setKeyMessages(response.keyMessages);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
errorMessage(error.message);
|
||||
} else {
|
||||
errorMessage(t("guideLineSetting.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function savePersonal(): Promise<void> {
|
||||
try {
|
||||
const response: boolean =
|
||||
await System.authPostToServer<boolean>(
|
||||
'book/guide-line',
|
||||
{
|
||||
bookId: bookId,
|
||||
tone: tone,
|
||||
atmosphere: atmosphere,
|
||||
writingStyle: writingStyle,
|
||||
themes: themes,
|
||||
symbolism: symbolism,
|
||||
motifs: motifs,
|
||||
narrativeVoice: narrativeVoice,
|
||||
pacing: pacing,
|
||||
intendedAudience: intendedAudience,
|
||||
keyMessages: keyMessages,
|
||||
},
|
||||
userToken,
|
||||
lang,
|
||||
);
|
||||
if (!response) {
|
||||
errorMessage(t("guideLineSetting.saveError"));
|
||||
return;
|
||||
}
|
||||
successMessage(t("guideLineSetting.saveSuccess"));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
errorMessage(error.message);
|
||||
} else {
|
||||
errorMessage(t("guideLineSetting.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveQuillSense(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPostToServer<boolean>(
|
||||
'quillsense/book/guide-line',
|
||||
{
|
||||
bookId: bookId,
|
||||
plotSummary: plotSummary,
|
||||
verbTense: verbTense,
|
||||
narrativeType: narrativeType,
|
||||
dialogueType: dialogueType,
|
||||
toneAtmosphere: toneAtmosphere,
|
||||
language: language,
|
||||
themes: themes,
|
||||
},
|
||||
userToken,
|
||||
lang,
|
||||
);
|
||||
if (response) {
|
||||
successMessage(t("guideLineSetting.saveSuccess"));
|
||||
} else {
|
||||
errorMessage(t("guideLineSetting.saveError"));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("guideLineSetting.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-2 border-b border-secondary/50 mb-6">
|
||||
<button
|
||||
className={`px-5 py-2.5 font-medium rounded-t-xl transition-all duration-200 ${
|
||||
activeTab === 'personal'
|
||||
? 'border-b-2 border-primary text-primary bg-primary/10 shadow-md'
|
||||
: 'text-text-secondary hover:text-text-primary hover:bg-secondary/30'
|
||||
}`}
|
||||
onClick={(): void => setActiveTab('personal')}
|
||||
>
|
||||
{t("guideLineSetting.personal")}
|
||||
</button>
|
||||
<button
|
||||
className={`px-5 py-2.5 font-medium rounded-t-xl transition-all duration-200 ${
|
||||
activeTab === 'quillsense'
|
||||
? 'border-b-2 border-primary text-primary bg-primary/10 shadow-md'
|
||||
: 'text-text-secondary hover:text-text-primary hover:bg-secondary/30'
|
||||
}`}
|
||||
onClick={() => setActiveTab('quillsense')}
|
||||
>
|
||||
{t("guideLineSetting.quillsense")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'personal' && (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.tone")} input={
|
||||
<TexteAreaInput
|
||||
value={tone}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setTone(e.target.value)}
|
||||
placeholder={t("guideLineSetting.tonePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.atmosphere")} input={
|
||||
<TexteAreaInput
|
||||
value={atmosphere}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setAtmosphere(e.target.value)}
|
||||
placeholder={t("guideLineSetting.atmospherePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.writingStyle")} input={
|
||||
<TexteAreaInput
|
||||
value={writingStyle}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setWritingStyle(e.target.value)}
|
||||
placeholder={t("guideLineSetting.writingStylePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.themes")} input={
|
||||
<TexteAreaInput
|
||||
value={themes}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setThemes(e.target.value)}
|
||||
placeholder={t("guideLineSetting.themesPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.symbolism")} input={
|
||||
<TexteAreaInput
|
||||
value={symbolism}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setSymbolism(e.target.value)}
|
||||
placeholder={t("guideLineSetting.symbolismPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.motifs")} input={
|
||||
<TexteAreaInput
|
||||
value={motifs}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setMotifs(e.target.value)}
|
||||
placeholder={t("guideLineSetting.motifsPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.narrativeVoice")} input={
|
||||
<TexteAreaInput
|
||||
value={narrativeVoice}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setNarrativeVoice(e.target.value)}
|
||||
placeholder={t("guideLineSetting.narrativeVoicePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.pacing")} input={
|
||||
<TexteAreaInput
|
||||
value={pacing}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setPacing(e.target.value)}
|
||||
placeholder={t("guideLineSetting.pacingPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.intendedAudience")} input={
|
||||
<TexteAreaInput
|
||||
value={intendedAudience}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setIntendedAudience(e.target.value)}
|
||||
placeholder={t("guideLineSetting.intendedAudiencePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.keyMessages")} input={
|
||||
<TexteAreaInput
|
||||
value={keyMessages}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setKeyMessages(e.target.value)}
|
||||
placeholder={t("guideLineSetting.keyMessagesPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'quillsense' && (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.plotSummary")} input={
|
||||
<TexteAreaInput
|
||||
value={plotSummary}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setPlotSummary(e.target.value)}
|
||||
placeholder={t("guideLineSetting.plotSummaryPlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.toneAtmosphere")} input={
|
||||
<TextInput
|
||||
value={toneAtmosphere}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>): void => setToneAtmosphere(e.target.value)}
|
||||
placeholder={t("guideLineSetting.toneAtmospherePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.themes")} input={
|
||||
<TextInput
|
||||
value={themes}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setThemes(e.target.value)}
|
||||
placeholder={t("guideLineSetting.themesPlaceholderQuill")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.verbTense")} input={
|
||||
<SelectBox
|
||||
defaultValue={verbTense}
|
||||
onChangeCallBack={(event: ChangeEvent<HTMLSelectElement>): void => setVerbTense(event.target.value)}
|
||||
data={verbalTime}
|
||||
placeholder={t("guideLineSetting.verbTensePlaceholder")}
|
||||
/>
|
||||
}/>
|
||||
</div>
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.narrativeType")} input={
|
||||
<SelectBox defaultValue={narrativeType} data={
|
||||
authorLevel === '1'
|
||||
? beginnerNarrativePersons
|
||||
: authorLevel === '2'
|
||||
? intermediateNarrativePersons
|
||||
: advancedNarrativePersons
|
||||
} onChangeCallBack={(event: ChangeEvent<HTMLSelectElement>): void => {
|
||||
setNarrativeType(event.target.value)
|
||||
}} placeholder={t("guideLineSetting.narrativeTypePlaceholder")}/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.dialogueType")} input={
|
||||
<SelectBox defaultValue={dialogueType} data={authorLevel === '1'
|
||||
? beginnerDialogueTypes
|
||||
: authorLevel === '2'
|
||||
? intermediateDialogueTypes
|
||||
: advancedDialogueTypes}
|
||||
onChangeCallBack={(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
setDialogueType(event.target.value)
|
||||
}} placeholder={t("guideLineSetting.dialogueTypePlaceholder")}/>
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
|
||||
<InputField fieldName={t("guideLineSetting.language")} input={
|
||||
<SelectBox defaultValue={language} data={langues}
|
||||
onChangeCallBack={(event: ChangeEvent<HTMLSelectElement>) => {
|
||||
setLanguage(event.target.value)
|
||||
}} placeholder={t("guideLineSetting.languagePlaceholder")}/>
|
||||
}/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(GuideLineSetting);
|
||||
444
components/book/settings/locations/LocationComponent.tsx
Normal file
444
components/book/settings/locations/LocationComponent.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
'use client'
|
||||
import {faMapMarkerAlt, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import System from '@/lib/models/System';
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from '@/components/form/TextInput';
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface SubElement {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Element {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
subElements: SubElement[];
|
||||
}
|
||||
|
||||
interface LocationProps {
|
||||
id: string;
|
||||
name: string;
|
||||
elements: Element[];
|
||||
}
|
||||
|
||||
export function LocationComponent(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {successMessage, errorMessage} = useContext(AlertContext);
|
||||
const {book} = useContext(BookContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [sections, setSections] = useState<LocationProps[]>([]);
|
||||
const [newSectionName, setNewSectionName] = useState<string>('');
|
||||
const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({});
|
||||
const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({});
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleSave,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getAllLocations().then();
|
||||
}, []);
|
||||
|
||||
async function getAllLocations(): Promise<void> {
|
||||
try {
|
||||
const response: LocationProps[] = await System.authGetQueryToServer<LocationProps[]>(`location/all`, token, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
if (response && response.length > 0) {
|
||||
setSections(response);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownFetchLocations'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddSection(): Promise<void> {
|
||||
if (!newSectionName.trim()) {
|
||||
errorMessage(t('locationComponent.errorSectionNameEmpty'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const sectionId: string = await System.authPostToServer<string>(`location/section/add`, {
|
||||
bookId: bookId,
|
||||
locationName: newSectionName,
|
||||
}, token, lang);
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
}
|
||||
const newLocation: LocationProps = {
|
||||
id: sectionId,
|
||||
name: newSectionName,
|
||||
elements: [],
|
||||
};
|
||||
setSections([...sections, newLocation]);
|
||||
setNewSectionName('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddElement(sectionId: string): Promise<void> {
|
||||
if (!newElementNames[sectionId]?.trim()) {
|
||||
errorMessage(t('locationComponent.errorElementNameEmpty'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
const elementId: string = await System.authPostToServer<string>(`location/element/add`, {
|
||||
bookId: bookId,
|
||||
locationId: sectionId,
|
||||
elementName: newElementNames[sectionId],
|
||||
},
|
||||
token, lang);
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
}
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
const sectionIndex: number = updatedSections.findIndex(
|
||||
(section: LocationProps): boolean => section.id === sectionId,
|
||||
);
|
||||
updatedSections[sectionIndex].elements.push({
|
||||
id: elementId,
|
||||
name: newElementNames[sectionId],
|
||||
description: '',
|
||||
subElements: [],
|
||||
});
|
||||
setSections(updatedSections);
|
||||
setNewElementNames({...newElementNames, [sectionId]: ''});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleElementChange(
|
||||
sectionId: string,
|
||||
elementIndex: number,
|
||||
field: keyof Element,
|
||||
value: string,
|
||||
): void {
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
const sectionIndex: number = updatedSections.findIndex(
|
||||
(section: LocationProps): boolean => section.id === sectionId,
|
||||
);
|
||||
// @ts-ignore
|
||||
updatedSections[sectionIndex].elements[elementIndex][field] = value;
|
||||
setSections(updatedSections);
|
||||
}
|
||||
|
||||
async function handleAddSubElement(
|
||||
sectionId: string,
|
||||
elementIndex: number,
|
||||
): Promise<void> {
|
||||
if (!newSubElementNames[elementIndex]?.trim()) {
|
||||
errorMessage(t('locationComponent.errorSubElementNameEmpty'))
|
||||
return
|
||||
}
|
||||
const sectionIndex: number = sections.findIndex(
|
||||
(section: LocationProps): boolean => section.id === sectionId,
|
||||
);
|
||||
try {
|
||||
const subElementId: string = await System.authPostToServer<string>(`location/sub-element/add`, {
|
||||
elementId: sections[sectionIndex].elements[elementIndex].id,
|
||||
subElementName: newSubElementNames[elementIndex],
|
||||
}, token, lang);
|
||||
if (!subElementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
return;
|
||||
}
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
updatedSections[sectionIndex].elements[elementIndex].subElements.push({
|
||||
id: subElementId,
|
||||
name: newSubElementNames[elementIndex],
|
||||
description: '',
|
||||
});
|
||||
setSections(updatedSections);
|
||||
setNewSubElementNames({...newSubElementNames, [elementIndex]: ''});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubElementChange(
|
||||
sectionId: string,
|
||||
elementIndex: number,
|
||||
subElementIndex: number,
|
||||
field: keyof SubElement,
|
||||
value: string,
|
||||
): void {
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
const sectionIndex: number = updatedSections.findIndex(
|
||||
(section: LocationProps): boolean => section.id === sectionId,
|
||||
);
|
||||
updatedSections[sectionIndex].elements[elementIndex].subElements[
|
||||
subElementIndex
|
||||
][field] = value;
|
||||
setSections(updatedSections);
|
||||
}
|
||||
|
||||
async function handleRemoveElement(
|
||||
sectionId: string,
|
||||
elementIndex: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(`location/element/delete`, {
|
||||
elementId: sections.find((section: LocationProps): boolean => section.id === sectionId)
|
||||
?.elements[elementIndex].id,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
return;
|
||||
}
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
const sectionIndex: number = updatedSections.findIndex((section: LocationProps): boolean => section.id === sectionId,);
|
||||
updatedSections[sectionIndex].elements.splice(elementIndex, 1);
|
||||
setSections(updatedSections);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveSubElement(
|
||||
sectionId: string,
|
||||
elementIndex: number,
|
||||
subElementIndex: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
|
||||
subElementId: sections.find((section: LocationProps): boolean => section.id === sectionId)?.elements[elementIndex].subElements[subElementIndex].id,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
return;
|
||||
}
|
||||
const updatedSections: LocationProps[] = [...sections];
|
||||
const sectionIndex: number = updatedSections.findIndex((section: LocationProps): boolean => section.id === sectionId,);
|
||||
updatedSections[sectionIndex].elements[elementIndex].subElements.splice(subElementIndex, 1,);
|
||||
setSections(updatedSections);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemoveSection(sectionId: string): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(`location/delete`, {
|
||||
locationId: sectionId,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
return;
|
||||
}
|
||||
const updatedSections: LocationProps[] = sections.filter((section: LocationProps): boolean => section.id !== sectionId,);
|
||||
setSections(updatedSections);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPostToServer<boolean>(`location/update`, {
|
||||
locations: sections,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
return;
|
||||
}
|
||||
successMessage(t('locationComponent.successSave'));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-4 border border-secondary/50">
|
||||
<div className="grid grid-cols-1 gap-4 mb-4">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={newSectionName}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setNewSectionName(e.target.value)}
|
||||
placeholder={t("locationComponent.newSectionPlaceholder")}
|
||||
/>
|
||||
}
|
||||
actionIcon={faPlus}
|
||||
actionLabel={t("locationComponent.addSectionLabel")}
|
||||
addButtonCallBack={handleAddSection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sections.length > 0 ? (
|
||||
sections.map((section: LocationProps) => (
|
||||
<div key={section.id}
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-4 border border-secondary/50">
|
||||
<h3 className="text-lg font-semibold text-text-primary mb-4 flex items-center">
|
||||
<FontAwesomeIcon icon={faMapMarkerAlt} className="mr-2 w-5 h-5"/>
|
||||
{section.name}
|
||||
<span
|
||||
className="ml-2 text-sm bg-dark-background text-text-secondary py-0.5 px-2 rounded-full">
|
||||
{section.elements.length || 0}
|
||||
</span>
|
||||
<button onClick={(): Promise<void> => handleRemoveSection(section.id)}
|
||||
className="ml-auto bg-dark-background text-text-primary rounded-full p-1.5 hover:bg-secondary transition-colors shadow-md">
|
||||
<FontAwesomeIcon icon={faTrash} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{section.elements.length > 0 ? (
|
||||
section.elements.map((element, elementIndex) => (
|
||||
<div key={element.id}
|
||||
className="bg-dark-background rounded-lg p-3 border-l-4 border-primary">
|
||||
<div className="mb-2">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={element.name}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleElementChange(section.id, elementIndex, 'name', e.target.value)
|
||||
}
|
||||
placeholder={t("locationComponent.elementNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
removeButtonCallBack={(): Promise<void> => handleRemoveElement(section.id, elementIndex)}
|
||||
/>
|
||||
</div>
|
||||
<TexteAreaInput
|
||||
value={element.description}
|
||||
setValue={(e: React.ChangeEvent<HTMLTextAreaElement>): void => handleElementChange(section.id, elementIndex, 'description', e.target.value)}
|
||||
placeholder={t("locationComponent.elementDescriptionPlaceholder")}
|
||||
/>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-secondary/50">
|
||||
{element.subElements.length > 0 && (
|
||||
<h4 className="text-sm italic text-text-secondary mb-3">{t("locationComponent.subElementsHeading")}</h4>
|
||||
)}
|
||||
|
||||
{element.subElements.map((subElement: SubElement, subElementIndex: number) => (
|
||||
<div key={subElement.id}
|
||||
className="bg-darkest-background rounded-lg p-3 mb-3">
|
||||
<div className="mb-2">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={subElement.name}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
handleSubElementChange(section.id, elementIndex, subElementIndex, 'name', e.target.value)
|
||||
}
|
||||
placeholder={t("locationComponent.subElementNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
removeButtonCallBack={(): Promise<void> => handleRemoveSubElement(section.id, elementIndex, subElementIndex)}
|
||||
/>
|
||||
</div>
|
||||
<TexteAreaInput
|
||||
value={subElement.description}
|
||||
setValue={(e) =>
|
||||
handleSubElementChange(section.id, elementIndex, subElementIndex, 'description', e.target.value)
|
||||
}
|
||||
placeholder={t("locationComponent.subElementDescriptionPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={newSubElementNames[elementIndex] || ''}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setNewSubElementNames({
|
||||
...newSubElementNames,
|
||||
[elementIndex]: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder={t("locationComponent.newSubElementPlaceholder")}
|
||||
/>
|
||||
}
|
||||
addButtonCallBack={(): Promise<void> => handleAddSubElement(section.id, elementIndex)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-text-secondary italic">
|
||||
{t("locationComponent.noElementAvailable")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={newElementNames[section.id] || ''}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setNewElementNames({...newElementNames, [section.id]: e.target.value})
|
||||
}
|
||||
placeholder={t("locationComponent.newElementPlaceholder")}
|
||||
/>
|
||||
}
|
||||
addButtonCallBack={(): Promise<void> => handleAddElement(section.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-8 border border-secondary/50 text-center">
|
||||
<p className="text-text-secondary mb-4">{t("locationComponent.noSectionAvailable")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(LocationComponent);
|
||||
327
components/book/settings/objects/page.tsx
Normal file
327
components/book/settings/objects/page.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
'use client';
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface RelatedItem {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
history: string;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: number | null;
|
||||
name: string;
|
||||
description: string;
|
||||
history: string;
|
||||
location: string;
|
||||
ownedBy: string;
|
||||
functionality: string;
|
||||
image: string;
|
||||
relatedItems: RelatedItem[];
|
||||
}
|
||||
|
||||
const initialItemState: Item = {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
history: '',
|
||||
location: '',
|
||||
ownedBy: '',
|
||||
functionality: '',
|
||||
image: '',
|
||||
relatedItems: [],
|
||||
};
|
||||
|
||||
export default function Items() {
|
||||
const [items, setItems] = useState<Item[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Sword of Destiny',
|
||||
description: 'A powerful sword',
|
||||
history: 'Forged in the ancient times...',
|
||||
location: 'Castle',
|
||||
ownedBy: 'John Doe',
|
||||
functionality: 'Cuts through anything',
|
||||
image: 'https://via.placeholder.com/150',
|
||||
relatedItems: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Shield of Valor',
|
||||
description: 'An unbreakable shield',
|
||||
history: 'Used by the legendary hero...',
|
||||
location: 'Fortress',
|
||||
ownedBy: 'Jane Doe',
|
||||
functionality: 'Deflects any attack',
|
||||
image: 'https://via.placeholder.com/150',
|
||||
relatedItems: []
|
||||
}
|
||||
]);
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [newItem, setNewItem] = useState<Item>(initialItemState);
|
||||
const [newRelatedItem, setNewRelatedItem] = useState<RelatedItem>({
|
||||
name: '',
|
||||
type: '',
|
||||
description: '',
|
||||
history: ''
|
||||
});
|
||||
|
||||
const filteredItems = items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleItemClick = (item: Item) => {
|
||||
setSelectedItem(item);
|
||||
};
|
||||
|
||||
const handleAddItem = () => {
|
||||
setSelectedItem(newItem);
|
||||
};
|
||||
|
||||
const handleSaveItem = () => {
|
||||
if (selectedItem) {
|
||||
if (selectedItem.id === null) {
|
||||
setItems([...items, {...selectedItem, id: items.length + 1}]);
|
||||
} else {
|
||||
setItems(items.map((item) => (item.id === selectedItem.id ? selectedItem : item)));
|
||||
}
|
||||
setSelectedItem(null);
|
||||
setNewItem(initialItemState);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemChange = (key: keyof Item, value: string) => {
|
||||
if (selectedItem) {
|
||||
setSelectedItem({...selectedItem, [key]: value});
|
||||
}
|
||||
};
|
||||
|
||||
const handleElementChange = (section: keyof Item, index: number, key: keyof RelatedItem, value: string) => {
|
||||
if (selectedItem) {
|
||||
const updatedSection = [...(selectedItem[section] as RelatedItem[])];
|
||||
updatedSection[index][key] = value;
|
||||
setSelectedItem({...selectedItem, [section]: updatedSection});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddElement = (section: keyof Item, value: RelatedItem) => {
|
||||
if (selectedItem) {
|
||||
const updatedSection = [...(selectedItem[section] as RelatedItem[]), value];
|
||||
setSelectedItem({...selectedItem, [section]: updatedSection});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveElement = (section: keyof Item, index: number) => {
|
||||
if (selectedItem) {
|
||||
const updatedSection = (selectedItem[section] as RelatedItem[]).filter((_, i) => i !== index);
|
||||
setSelectedItem({...selectedItem, [section]: updatedSection});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-grow p-8 overflow-y-auto">
|
||||
{selectedItem ? (
|
||||
<div>
|
||||
<div className="flex justify-between sticky top-0 z-10 bg-gray-900 py-4">
|
||||
<button onClick={() => setSelectedItem(null)}
|
||||
className="flex items-center gap-2 text-text-primary bg-secondary/50 hover:bg-secondary px-4 py-2 rounded-xl transition-all duration-200 hover:scale-105 shadow-md">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="mr-2 w-5 h-5"/> Back
|
||||
</button>
|
||||
<h2 className="text-3xl font-['ADLaM_Display'] text-center text-text-primary">{selectedItem.name}</h2>
|
||||
<button onClick={handleSaveItem}
|
||||
className="bg-primary hover:bg-primary-dark text-text-primary font-semibold py-2.5 px-6 rounded-xl shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200">
|
||||
Save Item
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-gray-700 rounded-lg p-8 shadow-lg space-y-4">
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={selectedItem.name}
|
||||
onChange={(e) => handleItemChange('name', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="description">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
rows={4}
|
||||
value={selectedItem.description}
|
||||
onChange={(e) => handleItemChange('description', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="history">History</label>
|
||||
<textarea
|
||||
id="history"
|
||||
rows={4}
|
||||
value={selectedItem.history}
|
||||
onChange={(e) => handleItemChange('history', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="location">Location</label>
|
||||
<select
|
||||
id="location"
|
||||
value={selectedItem.location}
|
||||
onChange={(e) => handleItemChange('location', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
>
|
||||
<option value="">Select Location</option>
|
||||
<option value="Castle">Castle</option>
|
||||
<option value="Fortress">Fortress</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="ownedBy">Owned By</label>
|
||||
<select
|
||||
id="ownedBy"
|
||||
value={selectedItem.ownedBy}
|
||||
onChange={(e) => handleItemChange('ownedBy', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
>
|
||||
<option value="">Select Owner</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.name}>{item.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="functionality">Functionality</label>
|
||||
<textarea
|
||||
id="functionality"
|
||||
rows={4}
|
||||
value={selectedItem.functionality}
|
||||
onChange={(e) => handleItemChange('functionality', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white mb-2" htmlFor="image">Image URL</label>
|
||||
<input
|
||||
type="text"
|
||||
id="image"
|
||||
value={selectedItem.image}
|
||||
onChange={(e) => handleItemChange('image', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-700 rounded-lg p-8 shadow-lg space-y-4 mt-4">
|
||||
<h3 className="text-2xl font-['ADLaM_Display'] text-text-primary">Related Items</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedItem.relatedItems.map((relatedItem, index) => (
|
||||
<details key={index}
|
||||
className="bg-secondary/30 rounded-xl mb-4 p-4 shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<summary className="text-lg text-white cursor-pointer">{relatedItem.name}</summary>
|
||||
<div className="mt-2">
|
||||
<label className="block text-white mb-2"
|
||||
htmlFor={`related-item-description-${relatedItem.name}`}>Description</label>
|
||||
<textarea
|
||||
id={`related-item-description-${relatedItem.name}`}
|
||||
rows={3}
|
||||
value={relatedItem.description}
|
||||
onChange={(e) => handleElementChange('relatedItems', index, 'description', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
></textarea>
|
||||
<label className="block text-white mb-2 mt-4"
|
||||
htmlFor={`related-item-history-${relatedItem.name}`}>History</label>
|
||||
<textarea
|
||||
id={`related-item-history-${relatedItem.name}`}
|
||||
rows={3}
|
||||
value={relatedItem.history}
|
||||
onChange={(e) => handleElementChange('relatedItems', index, 'history', e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveElement('relatedItems', index)}
|
||||
className="bg-error/90 hover:bg-error text-text-primary font-semibold py-2 px-4 rounded-xl shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200 mt-2"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
<div className="flex space-x-2 items-center mt-2">
|
||||
<select
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
onChange={(e) => setNewRelatedItem({...newRelatedItem, name: e.target.value})}
|
||||
value={newRelatedItem.name}
|
||||
>
|
||||
<option value="">Select Related Item</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.name}>{item.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
onChange={(e) => setNewRelatedItem({...newRelatedItem, type: e.target.value})}
|
||||
value={newRelatedItem.type}
|
||||
>
|
||||
<option value="">Relation Type</option>
|
||||
<option value="Related">Related</option>
|
||||
<option value="Similar">Similar</option>
|
||||
{/* Add more relation types as needed */}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
handleAddElement('relatedItems', {...newRelatedItem});
|
||||
setNewRelatedItem({name: '', type: '', description: '', history: ''});
|
||||
}}
|
||||
className="bg-primary hover:bg-primary-dark text-text-primary font-semibold py-2.5 px-5 rounded-xl shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||
>
|
||||
Add Related Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex justify-between sticky top-0 z-10 bg-gray-900 py-4">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl bg-secondary/50 text-text-primary border border-secondary/50 outline-none hover:bg-secondary hover:border-secondary focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-200"
|
||||
placeholder="Search Items"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddItem}
|
||||
className="bg-primary hover:bg-primary-dark text-text-primary font-semibold py-2.5 px-5 rounded-xl ml-4 shadow-md hover:shadow-lg hover:scale-105 transition-all duration-200"
|
||||
>
|
||||
Add New Item
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-4xl font-['ADLaM_Display'] text-text-primary mb-6">Items</h2>
|
||||
<div className="flex flex-wrap space-x-4">
|
||||
{filteredItems.map((item) => (
|
||||
<div key={item.id} onClick={() => handleItemClick(item)}
|
||||
className="cursor-pointer bg-tertiary/90 backdrop-blur-sm p-4 rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200 border border-secondary/50">
|
||||
<img src={item.image || 'https://via.placeholder.com/150'} alt={item.name}
|
||||
className="w-full h-32 object-cover rounded-lg mb-2"/>
|
||||
<h3 className="text-lg font-bold text-text-primary">{item.name}</h3>
|
||||
<p className="text-muted">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
608
components/book/settings/story/Act.tsx
Normal file
608
components/book/settings/story/Act.tsx
Normal file
@@ -0,0 +1,608 @@
|
||||
import React, {Dispatch, SetStateAction, useContext, useState} from 'react';
|
||||
import {
|
||||
faFire,
|
||||
faFlag,
|
||||
faPuzzlePiece,
|
||||
faScaleBalanced,
|
||||
faTrophy,
|
||||
IconDefinition,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Act as ActType, Incident, PlotPoint} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import System from '@/lib/models/System';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import CollapsableArea from '@/components/CollapsableArea';
|
||||
import ActDescription from '@/components/book/settings/story/act/ActDescription';
|
||||
import ActChaptersSection from '@/components/book/settings/story/act/ActChaptersSection';
|
||||
import ActIncidents from '@/components/book/settings/story/act/ActIncidents';
|
||||
import ActPlotPoints from '@/components/book/settings/story/act/ActPlotPoints';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface ActProps {
|
||||
acts: ActType[];
|
||||
setActs: Dispatch<SetStateAction<ActType[]>>;
|
||||
mainChapters: ChapterListProps[];
|
||||
}
|
||||
|
||||
export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [expandedSections, setExpandedSections] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({});
|
||||
|
||||
const [newIncidentTitle, setNewIncidentTitle] = useState<string>('');
|
||||
const [newPlotPointTitle, setNewPlotPointTitle] = useState<string>('');
|
||||
const [selectedIncidentId, setSelectedIncidentId] = useState<string>('');
|
||||
|
||||
function toggleSection(sectionKey: string): void {
|
||||
setExpandedSections(prev => ({
|
||||
...prev,
|
||||
[sectionKey]: !prev[sectionKey],
|
||||
}));
|
||||
}
|
||||
|
||||
function updateActSummary(actId: number, summary: string): void {
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
return {...act, summary};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
}
|
||||
|
||||
function getIncidents(): Incident[] {
|
||||
const act2: ActType | undefined = acts.find((act: ActType): boolean => act.id === 2);
|
||||
return act2?.incidents || [];
|
||||
}
|
||||
|
||||
async function addIncident(actId: number): Promise<void> {
|
||||
if (newIncidentTitle.trim() === '') return;
|
||||
|
||||
try {
|
||||
const incidentId: string =
|
||||
await System.authPostToServer<string>('book/incident/new', {
|
||||
bookId,
|
||||
name: newIncidentTitle,
|
||||
}, token, lang);
|
||||
if (!incidentId) {
|
||||
errorMessage(t('errorAddIncident'));
|
||||
return;
|
||||
}
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
const newIncident: Incident = {
|
||||
incidentId: incidentId,
|
||||
title: newIncidentTitle,
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
return {
|
||||
...act,
|
||||
incidents: [...(act.incidents || []), newIncident],
|
||||
};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
setNewIncidentTitle('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(t('errorAddIncident'));
|
||||
} else {
|
||||
errorMessage(t('errorUnknownAddIncident'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIncident(actId: number, incidentId: string): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('book/incident/remove', {
|
||||
bookId,
|
||||
incidentId,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('errorDeleteIncident'));
|
||||
return;
|
||||
}
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
return {
|
||||
...act,
|
||||
incidents: (act.incidents || []).filter(
|
||||
(inc: Incident): boolean => inc.incidentId !== incidentId,
|
||||
),
|
||||
};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('errorUnknownDeleteIncident'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addPlotPoint(actId: number): Promise<void> {
|
||||
if (newPlotPointTitle.trim() === '') return;
|
||||
try {
|
||||
const plotId: string = await System.authPostToServer<string>('book/plot/new', {
|
||||
bookId,
|
||||
name: newPlotPointTitle,
|
||||
incidentId: selectedIncidentId,
|
||||
}, token, lang);
|
||||
if (!plotId) {
|
||||
errorMessage(t('errorAddPlotPoint'));
|
||||
return;
|
||||
}
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
const newPlotPoint: PlotPoint = {
|
||||
plotPointId: plotId,
|
||||
title: newPlotPointTitle,
|
||||
summary: '',
|
||||
linkedIncidentId: selectedIncidentId,
|
||||
chapters: [],
|
||||
};
|
||||
return {
|
||||
...act,
|
||||
plotPoints: [...(act.plotPoints || []), newPlotPoint],
|
||||
};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
setNewPlotPointTitle('');
|
||||
setSelectedIncidentId('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(t('errorAddPlotPoint'));
|
||||
} else {
|
||||
errorMessage(t('errorUnknownAddPlotPoint'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('book/plot/remove', {
|
||||
plotId: plotPointId,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('errorDeletePlotPoint'));
|
||||
return;
|
||||
}
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
return {
|
||||
...act,
|
||||
plotPoints: (act.plotPoints || []).filter(
|
||||
(pp: PlotPoint): boolean => pp.plotPointId !== plotPointId,
|
||||
),
|
||||
};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('errorUnknownDeletePlotPoint'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function linkChapter(
|
||||
actId: number,
|
||||
chapterId: string,
|
||||
destination: 'act' | 'incident' | 'plotPoint',
|
||||
itemId?: string,
|
||||
): Promise<void> {
|
||||
const chapterToLink: ChapterListProps | undefined = mainChapters.find((chapter: ChapterListProps): boolean => chapter.chapterId === chapterId);
|
||||
if (!chapterToLink) {
|
||||
errorMessage(t('errorChapterNotFound'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const linkId: string =
|
||||
await System.authPostToServer<string>('chapter/resume/add', {
|
||||
bookId,
|
||||
chapterId: chapterId,
|
||||
actId: actId,
|
||||
plotId: destination === 'plotPoint' ? itemId : null,
|
||||
incidentId: destination === 'incident' ? itemId : null,
|
||||
}, token, lang);
|
||||
if (!linkId) {
|
||||
errorMessage(t('errorLinkChapter'));
|
||||
return;
|
||||
}
|
||||
const newChapter: ActChapter = {
|
||||
chapterInfoId: linkId,
|
||||
chapterId: chapterId,
|
||||
title: chapterToLink.title,
|
||||
chapterOrder: chapterToLink.chapterOrder || 0,
|
||||
actId: actId,
|
||||
incidentId: destination === 'incident' ? itemId : '0',
|
||||
plotPointId: destination === 'plotPoint' ? itemId : '0',
|
||||
summary: '',
|
||||
goal: '',
|
||||
};
|
||||
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
switch (destination) {
|
||||
case 'act':
|
||||
return {
|
||||
...act,
|
||||
chapters: [...(act.chapters || []), newChapter],
|
||||
};
|
||||
case 'incident':
|
||||
return {
|
||||
...act,
|
||||
incidents:
|
||||
act.incidents?.map((incident: Incident): Incident =>
|
||||
incident.incidentId === itemId
|
||||
? {
|
||||
...incident,
|
||||
chapters: [...(incident.chapters || []), newChapter],
|
||||
}
|
||||
: incident,
|
||||
) || [],
|
||||
};
|
||||
case 'plotPoint':
|
||||
return {
|
||||
...act,
|
||||
plotPoints:
|
||||
act.plotPoints?.map(
|
||||
(plotPoint: PlotPoint): PlotPoint =>
|
||||
plotPoint.plotPointId === itemId
|
||||
? {
|
||||
...plotPoint,
|
||||
chapters: [...(plotPoint.chapters || []), newChapter],
|
||||
}
|
||||
: plotPoint,
|
||||
) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return act;
|
||||
});
|
||||
|
||||
setActs(updatedActs);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('errorUnknownLinkChapter'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkChapter(
|
||||
chapterInfoId: string,
|
||||
actId: number,
|
||||
chapterId: string,
|
||||
destination: 'act' | 'incident' | 'plotPoint',
|
||||
itemId?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('chapter/resume/remove', {
|
||||
chapterInfoId,
|
||||
}, token, lang);
|
||||
if (!response) {
|
||||
errorMessage(t('errorUnlinkChapter'));
|
||||
return;
|
||||
}
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
switch (destination) {
|
||||
case 'act':
|
||||
return {
|
||||
...act,
|
||||
chapters: (act.chapters || []).filter(
|
||||
(ch: ActChapter): boolean => ch.chapterId !== chapterId,
|
||||
),
|
||||
};
|
||||
|
||||
case 'incident':
|
||||
if (!itemId) return act;
|
||||
|
||||
return {
|
||||
...act,
|
||||
incidents:
|
||||
act.incidents?.map((incident: Incident): Incident => {
|
||||
if (incident.incidentId === itemId) {
|
||||
return {
|
||||
...incident,
|
||||
chapters: (incident.chapters || []).filter(
|
||||
(ch: ActChapter): boolean =>
|
||||
ch.chapterId !== chapterId,
|
||||
),
|
||||
};
|
||||
}
|
||||
return incident;
|
||||
}) || [],
|
||||
};
|
||||
|
||||
case 'plotPoint':
|
||||
if (!itemId) return act;
|
||||
|
||||
return {
|
||||
...act,
|
||||
plotPoints:
|
||||
act.plotPoints?.map((plotPoint: PlotPoint): PlotPoint => {
|
||||
if (plotPoint.plotPointId === itemId) {
|
||||
return {
|
||||
...plotPoint,
|
||||
chapters: (plotPoint.chapters || []).filter((chapter: ActChapter): boolean => chapter.chapterId !== chapterId),
|
||||
};
|
||||
}
|
||||
return plotPoint;
|
||||
}) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return act;
|
||||
});
|
||||
|
||||
setActs(updatedActs);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('errorUnknownUnlinkChapter'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLinkedChapterSummary(
|
||||
actId: number,
|
||||
chapterId: string,
|
||||
summary: string,
|
||||
destination: 'act' | 'incident' | 'plotPoint',
|
||||
itemId?: string,
|
||||
): void {
|
||||
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
||||
if (act.id === actId) {
|
||||
switch (destination) {
|
||||
case 'act':
|
||||
return {
|
||||
...act,
|
||||
chapters: (act.chapters || []).map((chapter: ActChapter): ActChapter => {
|
||||
if (chapter.chapterId === chapterId) {
|
||||
return {...chapter, summary};
|
||||
}
|
||||
return chapter;
|
||||
}),
|
||||
};
|
||||
|
||||
case 'incident':
|
||||
if (!itemId) return act;
|
||||
|
||||
return {
|
||||
...act,
|
||||
incidents:
|
||||
act.incidents?.map((incident: Incident): Incident => {
|
||||
if (incident.incidentId === itemId) {
|
||||
return {
|
||||
...incident,
|
||||
chapters: (incident.chapters || []).map((chapter: ActChapter) => {
|
||||
if (chapter.chapterId === chapterId) {
|
||||
return {...chapter, summary};
|
||||
}
|
||||
return chapter;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return incident;
|
||||
}) || [],
|
||||
};
|
||||
|
||||
case 'plotPoint':
|
||||
if (!itemId) return act;
|
||||
|
||||
return {
|
||||
...act,
|
||||
plotPoints:
|
||||
act.plotPoints?.map((plotPoint: PlotPoint): PlotPoint => {
|
||||
if (plotPoint.plotPointId === itemId) {
|
||||
return {
|
||||
...plotPoint,
|
||||
chapters: (plotPoint.chapters || []).map((chapter: ActChapter): ActChapter => {
|
||||
if (chapter.chapterId === chapterId) {
|
||||
return {...chapter, summary};
|
||||
}
|
||||
return chapter;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return plotPoint;
|
||||
}) || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return act;
|
||||
});
|
||||
setActs(updatedActs);
|
||||
}
|
||||
|
||||
function getSectionKey(actId: number, section: string): string {
|
||||
return `section_${actId}_${section}`;
|
||||
}
|
||||
|
||||
|
||||
function renderActChapters(act: ActType) {
|
||||
if (act.id === 2 || act.id === 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sectionKey: string = getSectionKey(act.id, 'chapters');
|
||||
const isExpanded: boolean = expandedSections[sectionKey];
|
||||
|
||||
return (
|
||||
<ActChaptersSection
|
||||
actId={act.id}
|
||||
chapters={act.chapters || []}
|
||||
mainChapters={mainChapters}
|
||||
onLinkChapter={(actId, chapterId) => linkChapter(actId, chapterId, 'act')}
|
||||
onUpdateChapterSummary={(chapterId, summary) =>
|
||||
updateLinkedChapterSummary(act.id, chapterId, summary, 'act')
|
||||
}
|
||||
onUnlinkChapter={(chapterInfoId, chapterId) =>
|
||||
unlinkChapter(chapterInfoId, act.id, chapterId, 'act')
|
||||
}
|
||||
sectionKey={sectionKey}
|
||||
isExpanded={isExpanded}
|
||||
onToggleSection={toggleSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderActDescription(act: ActType) {
|
||||
if (act.id === 2 || act.id === 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActDescription
|
||||
actId={act.id}
|
||||
summary={act.summary || ''}
|
||||
onUpdateSummary={updateActSummary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderIncidents(act: ActType) {
|
||||
if (act.id !== 2) return null;
|
||||
|
||||
const sectionKey: string = getSectionKey(act.id, 'incidents');
|
||||
const isExpanded: boolean = expandedSections[sectionKey];
|
||||
|
||||
return (
|
||||
<ActIncidents
|
||||
incidents={act.incidents || []}
|
||||
actId={act.id}
|
||||
mainChapters={mainChapters}
|
||||
newIncidentTitle={newIncidentTitle}
|
||||
setNewIncidentTitle={setNewIncidentTitle}
|
||||
onAddIncident={addIncident}
|
||||
onDeleteIncident={deleteIncident}
|
||||
onLinkChapter={(actId, chapterId, incidentId) =>
|
||||
linkChapter(actId, chapterId, 'incident', incidentId)
|
||||
}
|
||||
onUpdateChapterSummary={(chapterId, summary, incidentId) =>
|
||||
updateLinkedChapterSummary(act.id, chapterId, summary, 'incident', incidentId)
|
||||
}
|
||||
onUnlinkChapter={(chapterInfoId, chapterId, incidentId) =>
|
||||
unlinkChapter(chapterInfoId, act.id, chapterId, 'incident', incidentId)
|
||||
}
|
||||
sectionKey={sectionKey}
|
||||
isExpanded={isExpanded}
|
||||
onToggleSection={toggleSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPlotPoints(act: ActType) {
|
||||
if (act.id !== 3) return null;
|
||||
|
||||
const sectionKey: string = getSectionKey(act.id, 'plotPoints');
|
||||
const isExpanded: boolean = expandedSections[sectionKey];
|
||||
|
||||
return (
|
||||
<ActPlotPoints
|
||||
plotPoints={act.plotPoints || []}
|
||||
incidents={getIncidents()}
|
||||
actId={act.id}
|
||||
mainChapters={mainChapters}
|
||||
newPlotPointTitle={newPlotPointTitle}
|
||||
setNewPlotPointTitle={setNewPlotPointTitle}
|
||||
selectedIncidentId={selectedIncidentId}
|
||||
setSelectedIncidentId={setSelectedIncidentId}
|
||||
onAddPlotPoint={addPlotPoint}
|
||||
onDeletePlotPoint={deletePlotPoint}
|
||||
onLinkChapter={(actId, chapterId, plotPointId) =>
|
||||
linkChapter(actId, chapterId, 'plotPoint', plotPointId)
|
||||
}
|
||||
onUpdateChapterSummary={(chapterId, summary, plotPointId) =>
|
||||
updateLinkedChapterSummary(act.id, chapterId, summary, 'plotPoint', plotPointId)
|
||||
}
|
||||
onUnlinkChapter={(chapterInfoId, chapterId, plotPointId) =>
|
||||
unlinkChapter(chapterInfoId, act.id, chapterId, 'plotPoint', plotPointId)
|
||||
}
|
||||
sectionKey={sectionKey}
|
||||
isExpanded={isExpanded}
|
||||
onToggleSection={toggleSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderActIcon(actId: number): IconDefinition {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return faFlag;
|
||||
case 2:
|
||||
return faFire;
|
||||
case 3:
|
||||
return faPuzzlePiece;
|
||||
case 4:
|
||||
return faScaleBalanced;
|
||||
case 5:
|
||||
return faTrophy;
|
||||
default:
|
||||
return faFlag;
|
||||
}
|
||||
}
|
||||
|
||||
function renderActTitle(actId: number): string {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return t('act1Title');
|
||||
case 2:
|
||||
return t('act2Title');
|
||||
case 3:
|
||||
return t('act3Title');
|
||||
case 4:
|
||||
return t('act4Title');
|
||||
case 5:
|
||||
return t('act5Title');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{acts.map((act: ActType) => (
|
||||
<CollapsableArea key={`act-${act.id}`}
|
||||
title={renderActTitle(act.id)}
|
||||
icon={renderActIcon(act.id)}
|
||||
children={
|
||||
<>
|
||||
{renderActDescription(act)}
|
||||
{renderActChapters(act)}
|
||||
{renderIncidents(act)}
|
||||
{renderPlotPoints(act)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
149
components/book/settings/story/Issue.tsx
Normal file
149
components/book/settings/story/Issue.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, {ChangeEvent, useContext, useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faPlus, faTrash, faWarning,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Issue} from '@/lib/models/Book';
|
||||
import System from '@/lib/models/System';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface IssuesProps {
|
||||
issues: Issue[];
|
||||
setIssues: React.Dispatch<React.SetStateAction<Issue[]>>;
|
||||
}
|
||||
|
||||
export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [newIssueName, setNewIssueName] = useState<string>('');
|
||||
|
||||
async function addNewIssue(): Promise<void> {
|
||||
if (newIssueName.trim() === '') {
|
||||
errorMessage(t("issues.errorEmptyName"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issueId: string = await System.authPostToServer<string>('book/issue/add', {
|
||||
bookId,
|
||||
name: newIssueName,
|
||||
}, token, lang);
|
||||
if (!issueId) {
|
||||
errorMessage(t("issues.errorAdd"));
|
||||
return;
|
||||
}
|
||||
const newIssue: Issue = {
|
||||
name: newIssueName,
|
||||
id: issueId,
|
||||
};
|
||||
|
||||
setIssues([...issues, newIssue]);
|
||||
setNewIssueName('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("issues.errorUnknownAdd"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIssue(issueId: string): Promise<void> {
|
||||
if (issueId === undefined) {
|
||||
errorMessage(t("issues.errorInvalidId"));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(
|
||||
'book/issue/remove',
|
||||
{
|
||||
bookId,
|
||||
issueId,
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
if (response) {
|
||||
const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,);
|
||||
setIssues(updatedIssues);
|
||||
} else {
|
||||
errorMessage(t("issues.errorDelete"));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("issues.errorUnknownDelete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateIssueName(issueId: string, name: string): void {
|
||||
const updatedIssues: Issue[] = issues.map((issue: Issue): Issue => {
|
||||
if (issue.id === issueId) {
|
||||
return {...issue, name};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
setIssues(updatedIssues);
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableArea title={t("issues.title")} children={
|
||||
<div className="p-1">
|
||||
{issues && issues.length > 0 ? (
|
||||
issues.map((item: Issue) => (
|
||||
<div
|
||||
className="mb-2 bg-secondary/30 rounded-xl p-3 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
key={`issue-${item.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={item.name}
|
||||
onChange={(e) => updateIssueName(item.id, e.target.value)}
|
||||
placeholder={t("issues.issueNamePlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="p-1.5 ml-2 rounded-lg text-error hover:bg-error/20 hover:scale-110 transition-all duration-200"
|
||||
onClick={() => deleteIssue(item.id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} size="sm"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center py-2 text-sm">
|
||||
{t("issues.noIssue")}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center mt-3 bg-secondary/30 p-3 rounded-xl shadow-sm">
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={newIssueName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewIssueName(e.target.value)}
|
||||
placeholder={t("issues.newIssuePlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary w-9 h-9 rounded-full flex justify-center items-center ml-2 text-text-primary shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
onClick={addNewIssue}
|
||||
disabled={newIssueName.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} icon={faWarning}/>
|
||||
);
|
||||
}
|
||||
278
components/book/settings/story/MainChapter.tsx
Normal file
278
components/book/settings/story/MainChapter.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
'use client'
|
||||
|
||||
import React, {ChangeEvent, useContext, useEffect, useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faArrowDown, faArrowUp, faBookmark, faMinus, faPlus, faTrash,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {ChapterListProps} from '@/lib/models/Chapter';
|
||||
import System from '@/lib/models/System';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import AlertBox from "@/components/AlertBox";
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext} from "@/context/LangContext";
|
||||
|
||||
interface MainChapterProps {
|
||||
chapters: ChapterListProps[];
|
||||
setChapters: React.Dispatch<React.SetStateAction<ChapterListProps[]>>;
|
||||
}
|
||||
|
||||
export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext(LangContext)
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [newChapterTitle, setNewChapterTitle] = useState<string>('');
|
||||
const [newChapterOrder, setNewChapterOrder] = useState<number>(0);
|
||||
|
||||
const [deleteConfirmMessage, setDeleteConfirmMessage] = useState<boolean>(false);
|
||||
const [chapterIdToRemove, setChapterIdToRemove] = useState<string>('');
|
||||
|
||||
function handleChapterTitleChange(chapterId: string, newTitle: string) {
|
||||
const updatedChapters: ChapterListProps[] = chapters.map((chapter: ChapterListProps): ChapterListProps => {
|
||||
if (chapter.chapterId === chapterId) {
|
||||
return {...chapter, title: newTitle};
|
||||
}
|
||||
return chapter;
|
||||
});
|
||||
setChapters(updatedChapters);
|
||||
}
|
||||
|
||||
function moveChapter(index: number, direction: number): void {
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number => (a.chapterOrder || 0) - (b.chapterOrder || 0));
|
||||
|
||||
const currentChapter: ChapterListProps = visibleChapters[index];
|
||||
const allChaptersIndex: number = chapters.findIndex(
|
||||
(chapter: ChapterListProps): boolean => chapter.chapterId === currentChapter.chapterId,
|
||||
);
|
||||
|
||||
const updatedChapters: ChapterListProps[] = [...chapters];
|
||||
|
||||
const currentOrder: number = updatedChapters[allChaptersIndex].chapterOrder || 0;
|
||||
const newOrder: number = Math.max(0, currentOrder + direction);
|
||||
|
||||
updatedChapters[allChaptersIndex] = {
|
||||
...updatedChapters[allChaptersIndex],
|
||||
chapterOrder: newOrder,
|
||||
};
|
||||
|
||||
setChapters(updatedChapters);
|
||||
}
|
||||
|
||||
function moveChapterUp(index: number): void {
|
||||
moveChapter(index, -1);
|
||||
}
|
||||
|
||||
function moveChapterDown(index: number): void {
|
||||
moveChapter(index, 1);
|
||||
}
|
||||
|
||||
async function deleteChapter(): Promise<void> {
|
||||
try {
|
||||
setDeleteConfirmMessage(false);
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(
|
||||
'chapter/remove',
|
||||
{
|
||||
bookId,
|
||||
chapterId: chapterIdToRemove,
|
||||
},
|
||||
token,
|
||||
lang,
|
||||
);
|
||||
if (!response) {
|
||||
errorMessage(t("mainChapter.errorDelete"));
|
||||
}
|
||||
const updatedChapters: ChapterListProps[] = chapters.filter((chapter: ChapterListProps): boolean => chapter.chapterId !== chapterIdToRemove,);
|
||||
setChapters(updatedChapters);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message)
|
||||
} else {
|
||||
errorMessage(t("mainChapter.errorUnknownDelete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewChapter(): Promise<void> {
|
||||
if (newChapterTitle.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const responseId: string = await System.authPostToServer<string>(
|
||||
'chapter/add',
|
||||
{
|
||||
bookId: bookId,
|
||||
wordsCount: 0,
|
||||
chapterOrder: newChapterOrder ? newChapterOrder : 0,
|
||||
title: newChapterTitle,
|
||||
},
|
||||
token,
|
||||
);
|
||||
if (!responseId) {
|
||||
errorMessage(t("mainChapter.errorAdd"));
|
||||
return;
|
||||
}
|
||||
const newChapter: ChapterListProps = {
|
||||
chapterId: responseId as string,
|
||||
title: newChapterTitle,
|
||||
chapterOrder: newChapterOrder,
|
||||
summary: '',
|
||||
goal: '',
|
||||
};
|
||||
setChapters([...chapters, newChapter]);
|
||||
setNewChapterTitle('');
|
||||
|
||||
setNewChapterOrder(newChapterOrder + 1);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message)
|
||||
} else {
|
||||
errorMessage(t("mainChapter.errorUnknownAdd"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decrementNewChapterOrder(): void {
|
||||
if (newChapterOrder > 0) {
|
||||
setNewChapterOrder(newChapterOrder - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function incrementNewChapterOrder(): void {
|
||||
setNewChapterOrder(newChapterOrder + 1);
|
||||
}
|
||||
|
||||
useEffect((): void => {
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number =>
|
||||
(a.chapterOrder || 0) - (b.chapterOrder || 0),
|
||||
);
|
||||
|
||||
const nextOrder: number =
|
||||
visibleChapters.length > 0
|
||||
? (visibleChapters[visibleChapters.length - 1].chapterOrder || 0) + 1
|
||||
: 0;
|
||||
|
||||
setNewChapterOrder(nextOrder);
|
||||
}, [chapters]);
|
||||
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number => (a.chapterOrder || 0) - (b.chapterOrder || 0));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CollapsableArea icon={faBookmark} title={t("mainChapter.chapters")} children={
|
||||
<div className="space-y-4">
|
||||
{visibleChapters.length > 0 ? (
|
||||
<div>
|
||||
{visibleChapters.map((item: ChapterListProps, index: number) => (
|
||||
<div key={item.chapterId}
|
||||
className="mb-2 bg-secondary/30 rounded-xl p-3 shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="bg-primary-dark text-white text-sm w-6 h-6 rounded-full text-center leading-6 mr-2">
|
||||
{item.chapterOrder !== undefined ? item.chapterOrder : index}
|
||||
</span>
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200"
|
||||
value={item.title}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => handleChapterTitleChange(item.chapterId, e.target.value)}
|
||||
placeholder={t("mainChapter.chapterTitlePlaceholder")}
|
||||
/>
|
||||
<div className="flex ml-1">
|
||||
<button
|
||||
className={`p-1.5 mx-0.5 rounded-lg hover:bg-secondary/50 transition-all duration-200 ${
|
||||
item.chapterOrder === 0 ? 'text-muted cursor-not-allowed' : 'text-primary hover:scale-110'
|
||||
}`}
|
||||
onClick={(): void => moveChapterUp(index)}
|
||||
disabled={item.chapterOrder === 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} size="sm"/>
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 mx-0.5 rounded-lg text-primary hover:bg-secondary/50 hover:scale-110 transition-all duration-200"
|
||||
onClick={(): void => moveChapterDown(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} size="sm"/>
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 mx-0.5 rounded-lg text-error hover:bg-error/20 hover:scale-110 transition-all duration-200"
|
||||
onClick={(): void => {
|
||||
setChapterIdToRemove(item.chapterId);
|
||||
setDeleteConfirmMessage(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} size="sm"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center py-2">
|
||||
{t("mainChapter.noChapter")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="bg-secondary/30 rounded-xl p-3 mt-3 shadow-sm">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="flex items-center gap-1 bg-secondary/50 rounded-lg mr-2 px-1 py-0.5 shadow-inner">
|
||||
<button
|
||||
className={`w-6 h-6 rounded-full bg-secondary flex justify-center items-center hover:scale-110 transition-all duration-200 ${
|
||||
newChapterOrder <= 0 ? 'text-muted cursor-not-allowed opacity-50' : 'text-primary hover:bg-secondary-dark'
|
||||
}`}
|
||||
onClick={decrementNewChapterOrder}
|
||||
disabled={newChapterOrder <= 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faMinus} size="xs"/>
|
||||
</button>
|
||||
<span
|
||||
className="bg-primary-dark text-white text-sm w-6 h-6 rounded-full text-center leading-6 mx-0.5">
|
||||
{newChapterOrder}
|
||||
</span>
|
||||
<button
|
||||
className="w-6 h-6 rounded-full bg-secondary flex justify-center items-center text-primary hover:bg-secondary-dark hover:scale-110 transition-all duration-200"
|
||||
onClick={incrementNewChapterOrder}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} size="xs"/>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={newChapterTitle}
|
||||
onChange={e => setNewChapterTitle(e.target.value)}
|
||||
placeholder={t("mainChapter.newChapterPlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary w-9 h-9 rounded-full flex justify-center items-center ml-2 text-text-primary shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
onClick={addNewChapter}
|
||||
disabled={newChapterTitle.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}/>
|
||||
{
|
||||
deleteConfirmMessage &&
|
||||
<AlertBox title={t("mainChapter.deleteTitle")} message={t("mainChapter.deleteMessage")}
|
||||
type={"danger"} onConfirm={deleteChapter}
|
||||
onCancel={(): void => setDeleteConfirmMessage(false)}/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
components/book/settings/story/StorySetting.tsx
Normal file
167
components/book/settings/story/StorySetting.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
|
||||
import React, {createContext, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import System from '@/lib/models/System';
|
||||
import {Act as ActType, Issue} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import MainChapter from "@/components/book/settings/story/MainChapter";
|
||||
import Issues from "@/components/book/settings/story/Issue";
|
||||
import Act from "@/components/book/settings/story/Act";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
export const StoryContext = createContext<{
|
||||
acts: ActType[];
|
||||
setActs: React.Dispatch<React.SetStateAction<ActType[]>>;
|
||||
mainChapters: ChapterListProps[];
|
||||
setMainChapters: React.Dispatch<React.SetStateAction<ChapterListProps[]>>;
|
||||
issues: Issue[];
|
||||
setIssues: React.Dispatch<React.SetStateAction<Issue[]>>;
|
||||
}>({
|
||||
acts: [],
|
||||
setActs: (): void => {
|
||||
},
|
||||
mainChapters: [],
|
||||
setMainChapters: (): void => {
|
||||
},
|
||||
issues: [],
|
||||
setIssues: (): void => {
|
||||
},
|
||||
});
|
||||
|
||||
interface StoryFetchData {
|
||||
mainChapter: ChapterListProps[];
|
||||
acts: ActType[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
export function Story(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const bookId: string = book?.bookId ? book.bookId.toString() : '';
|
||||
const {session} = useContext(SessionContext);
|
||||
const userToken: string = session.accessToken;
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
const [acts, setActs] = useState<ActType[]>([]);
|
||||
const [issues, setIssues] = useState<Issue[]>([]);
|
||||
const [mainChapters, setMainChapters] = useState<ChapterListProps[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleSave
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getStoryData().then();
|
||||
}, [userToken]);
|
||||
|
||||
useEffect((): void => {
|
||||
cleanupDeletedChapters();
|
||||
}, [mainChapters]);
|
||||
|
||||
async function getStoryData(): Promise<void> {
|
||||
try {
|
||||
const response: StoryFetchData = await System.authGetQueryToServer<StoryFetchData>(`book/story`, userToken, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
if (response) {
|
||||
setActs(response.acts);
|
||||
setMainChapters(response.mainChapter);
|
||||
setIssues(response.issues);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("story.errorUnknownFetch"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupDeletedChapters(): void {
|
||||
const existingChapterIds: string[] = mainChapters.map(ch => ch.chapterId);
|
||||
|
||||
const updatedActs = acts.map((act: ActType) => {
|
||||
const filteredChapters: ActChapter[] = act.chapters?.filter((chapter: ActChapter): boolean =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || [];
|
||||
const updatedIncidents = act.incidents?.map(incident => {
|
||||
return {
|
||||
...incident,
|
||||
chapters: incident.chapters?.filter(chapter =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || []
|
||||
};
|
||||
}) || [];
|
||||
const updatedPlotPoints = act.plotPoints?.map(plotPoint => {
|
||||
return {
|
||||
...plotPoint,
|
||||
chapters: plotPoint.chapters?.filter(chapter =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || []
|
||||
};
|
||||
}) || [];
|
||||
return {
|
||||
...act,
|
||||
chapters: filteredChapters,
|
||||
incidents: updatedIncidents,
|
||||
plotPoints: updatedPlotPoints,
|
||||
};
|
||||
});
|
||||
setActs(updatedActs);
|
||||
}
|
||||
|
||||
async function handleSave(): Promise<void> {
|
||||
try {
|
||||
const response: boolean =
|
||||
await System.authPostToServer<boolean>('book/story', {
|
||||
bookId,
|
||||
acts,
|
||||
mainChapters,
|
||||
issues,
|
||||
}, userToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("story.errorSave"))
|
||||
}
|
||||
successMessage(t("story.successSave"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("story.errorUnknownSave"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StoryContext.Provider
|
||||
value={{
|
||||
acts,
|
||||
setActs,
|
||||
mainChapters,
|
||||
setMainChapters,
|
||||
issues,
|
||||
setIssues,
|
||||
}}>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow overflow-auto py-4">
|
||||
<div className="space-y-6 px-4">
|
||||
<MainChapter chapters={mainChapters} setChapters={setMainChapters}/>
|
||||
<div className="space-y-4">
|
||||
<Act acts={acts} setActs={setActs} mainChapters={mainChapters}/>
|
||||
</div>
|
||||
<Issues issues={issues} setIssues={setIssues}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StoryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(Story);
|
||||
37
components/book/settings/story/act/ActChapter.tsx
Normal file
37
components/book/settings/story/act/ActChapter.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, {ChangeEvent} from 'react';
|
||||
import {faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {ActChapter} from '@/lib/models/Chapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import TexteAreaInput from '@/components/form/TexteAreaInput';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActChapterItemProps {
|
||||
chapter: ActChapter;
|
||||
onUpdateSummary: (chapterId: string, summary: string) => void;
|
||||
onUnlink: (chapterInfoId: string, chapterId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ActChapterItem({chapter, onUpdateSummary, onUnlink}: ActChapterItemProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-secondary/20 p-4 rounded-xl mb-3 border border-secondary/30 shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<InputField
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={chapter.summary || ''}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onUpdateSummary(chapter.chapterId, e.target.value)
|
||||
}
|
||||
placeholder={t('chapterSummaryPlaceholder')}
|
||||
/>
|
||||
}
|
||||
actionIcon={faTrash}
|
||||
fieldName={chapter.title}
|
||||
action={(): Promise<void> => onUnlink(chapter.chapterInfoId, chapter.chapterId)}
|
||||
actionLabel={t('remove')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
components/book/settings/story/act/ActChaptersSection.tsx
Normal file
93
components/book/settings/story/act/ActChaptersSection.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp} from '@fortawesome/free-solid-svg-icons';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import {SelectBoxProps} from '@/shared/interface';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import SelectBox from '@/components/form/SelectBox';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActChaptersSectionProps {
|
||||
actId: number;
|
||||
chapters: ActChapter[];
|
||||
mainChapters: ChapterListProps[];
|
||||
onLinkChapter: (actId: number, chapterId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActChaptersSection({
|
||||
actId,
|
||||
chapters,
|
||||
mainChapters,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActChaptersSectionProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function mainChaptersData(): SelectBoxProps[] {
|
||||
return mainChapters.map((chapter: ChapterListProps): SelectBoxProps => ({
|
||||
value: chapter.chapterId,
|
||||
label: `${chapter.chapterOrder}. ${chapter.title}`,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('chapters')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{chapters && chapters.length > 0 ? (
|
||||
chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`chapter-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId)
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
<InputField
|
||||
addButtonCallBack={(): Promise<void> => onLinkChapter(actId, selectedChapterId)}
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={null}
|
||||
onChangeCallBack={(e) => setSelectedChapterId(e.target.value)}
|
||||
data={mainChaptersData()}
|
||||
placeholder={t('selectChapterPlaceholder')}
|
||||
/>
|
||||
}
|
||||
isAddButtonDisabled={!selectedChapterId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
components/book/settings/story/act/ActDescription.tsx
Normal file
60
components/book/settings/story/act/ActDescription.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, {ChangeEvent} from 'react';
|
||||
import {faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import TexteAreaInput from '@/components/form/TexteAreaInput';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActDescriptionProps {
|
||||
actId: number;
|
||||
summary: string;
|
||||
onUpdateSummary: (actId: number, summary: string) => void;
|
||||
}
|
||||
|
||||
export default function ActDescription({actId, summary, onUpdateSummary}: ActDescriptionProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
|
||||
function getActSummaryTitle(actId: number): string {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return t('act1Summary');
|
||||
case 4:
|
||||
return t('act4Summary');
|
||||
case 5:
|
||||
return t('act5Summary');
|
||||
default:
|
||||
return t('actSummary');
|
||||
}
|
||||
}
|
||||
|
||||
function getActSummaryPlaceholder(actId: number): string {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return t('act1SummaryPlaceholder');
|
||||
case 4:
|
||||
return t('act4SummaryPlaceholder');
|
||||
case 5:
|
||||
return t('act5SummaryPlaceholder');
|
||||
default:
|
||||
return t('actSummaryPlaceholder');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<InputField
|
||||
fieldName={getActSummaryTitle(actId)}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={summary || ''}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onUpdateSummary(actId, e.target.value)
|
||||
}
|
||||
placeholder={getActSummaryPlaceholder(actId)}
|
||||
/>
|
||||
}
|
||||
actionIcon={faTrash}
|
||||
actionLabel={t('delete')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
components/book/settings/story/act/ActIncidents.tsx
Normal file
176
components/book/settings/story/act/ActIncidents.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Incident} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActIncidentsProps {
|
||||
incidents: Incident[];
|
||||
actId: number;
|
||||
mainChapters: ChapterListProps[];
|
||||
newIncidentTitle: string;
|
||||
setNewIncidentTitle: (title: string) => void;
|
||||
onAddIncident: (actId: number) => Promise<void>;
|
||||
onDeleteIncident: (actId: number, incidentId: string) => Promise<void>;
|
||||
onLinkChapter: (actId: number, chapterId: string, incidentId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string, incidentId: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string, incidentId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActIncidents({
|
||||
incidents,
|
||||
actId,
|
||||
mainChapters,
|
||||
newIncidentTitle,
|
||||
setNewIncidentTitle,
|
||||
onAddIncident,
|
||||
onDeleteIncident,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActIncidentsProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({});
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function toggleItem(itemKey: string): void {
|
||||
setExpandedItems(prev => ({
|
||||
...prev,
|
||||
[itemKey]: !prev[itemKey],
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('incidentsTitle')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{incidents && incidents.length > 0 ? (
|
||||
<>
|
||||
{incidents.map((item: Incident) => {
|
||||
const itemKey = `incident_${item.incidentId}`;
|
||||
const isItemExpanded: boolean = expandedItems[itemKey];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`incident-${item.incidentId}`}
|
||||
className="bg-secondary/30 rounded-xl mb-3 overflow-hidden border border-secondary/40 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<button
|
||||
className="flex justify-between items-center w-full p-2 text-left"
|
||||
onClick={(): void => toggleItem(itemKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{item.title}</span>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={isItemExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5 mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDeleteIncident(actId, item.incidentId);
|
||||
}}
|
||||
className="text-error hover:bg-error/20 p-1.5 rounded-lg transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isItemExpanded && (
|
||||
<div className="p-3 bg-secondary/20">
|
||||
{item.chapters && item.chapters.length > 0 ? (
|
||||
<>
|
||||
{item.chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`inc-chapter-${chapter.chapterId}-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary, item.incidentId)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId, item.incidentId)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<select
|
||||
onChange={(e) => setSelectedChapterId(e.target.value)}
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200"
|
||||
>
|
||||
<option value="">{t('selectChapterPlaceholder')}</option>
|
||||
{mainChapters.map((chapter: ChapterListProps) => (
|
||||
<option key={chapter.chapterId} value={chapter.chapterId}>
|
||||
{`${chapter.chapterOrder}. ${chapter.title}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={(): Promise<void> =>
|
||||
onLinkChapter(actId, selectedChapterId, item.incidentId)
|
||||
}
|
||||
disabled={selectedChapterId.length === 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noIncidentAdded')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200 placeholder:text-muted/60"
|
||||
value={newIncidentTitle}
|
||||
onChange={(e) => setNewIncidentTitle(e.target.value)}
|
||||
placeholder={t('newIncidentPlaceholder')}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={(): Promise<void> => onAddIncident(actId)}
|
||||
disabled={newIncidentTitle.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
202
components/book/settings/story/act/ActPlotPoints.tsx
Normal file
202
components/book/settings/story/act/ActPlotPoints.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Incident, PlotPoint} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import {SelectBoxProps} from '@/shared/interface';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import SelectBox from '@/components/form/SelectBox';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActPlotPointsProps {
|
||||
plotPoints: PlotPoint[];
|
||||
incidents: Incident[];
|
||||
actId: number;
|
||||
mainChapters: ChapterListProps[];
|
||||
newPlotPointTitle: string;
|
||||
setNewPlotPointTitle: (title: string) => void;
|
||||
selectedIncidentId: string;
|
||||
setSelectedIncidentId: (id: string) => void;
|
||||
onAddPlotPoint: (actId: number) => Promise<void>;
|
||||
onDeletePlotPoint: (actId: number, plotPointId: string) => Promise<void>;
|
||||
onLinkChapter: (actId: number, chapterId: string, plotPointId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string, plotPointId: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string, plotPointId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActPlotPoints({
|
||||
plotPoints,
|
||||
incidents,
|
||||
actId,
|
||||
mainChapters,
|
||||
newPlotPointTitle,
|
||||
setNewPlotPointTitle,
|
||||
selectedIncidentId,
|
||||
setSelectedIncidentId,
|
||||
onAddPlotPoint,
|
||||
onDeletePlotPoint,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActPlotPointsProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({});
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function toggleItem(itemKey: string): void {
|
||||
setExpandedItems(prev => ({
|
||||
...prev,
|
||||
[itemKey]: !prev[itemKey],
|
||||
}));
|
||||
}
|
||||
|
||||
function getIncidentData(): SelectBoxProps[] {
|
||||
return incidents.map((incident: Incident): SelectBoxProps => ({
|
||||
value: incident.incidentId,
|
||||
label: incident.title,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('plotPointsTitle')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{plotPoints && plotPoints.length > 0 ? (
|
||||
plotPoints.map((item: PlotPoint) => {
|
||||
const itemKey = `plotpoint_${item.plotPointId}`;
|
||||
const isItemExpanded: boolean = expandedItems[itemKey];
|
||||
const linkedIncident: Incident | undefined = incidents.find(
|
||||
(inc: Incident): boolean => inc.incidentId === item.linkedIncidentId
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`plot-point-${item.plotPointId}`}
|
||||
className="bg-secondary/30 rounded-xl mb-3 overflow-hidden border border-secondary/40 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<button
|
||||
className="flex justify-between items-center w-full p-2 text-left"
|
||||
onClick={(): void => toggleItem(itemKey)}
|
||||
>
|
||||
<div>
|
||||
<p className="font-bold text-text-primary">{item.title}</p>
|
||||
{linkedIncident && (
|
||||
<p className="text-text-secondary text-sm italic">
|
||||
{t('linkedTo')}: {linkedIncident.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={isItemExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5 mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={async (e): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
await onDeletePlotPoint(actId, item.plotPointId);
|
||||
}}
|
||||
className="text-error hover:bg-error/20 p-1.5 rounded-lg transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isItemExpanded && (
|
||||
<div className="p-3 bg-secondary/20">
|
||||
{item.chapters && item.chapters.length > 0 ? (
|
||||
item.chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`plot-chapter-${chapter.chapterId}-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary, item.plotPointId)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId, item.plotPointId)
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<select
|
||||
onChange={(e) => setSelectedChapterId(e.target.value)}
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200"
|
||||
>
|
||||
<option value="">{t('selectChapterPlaceholder')}</option>
|
||||
{mainChapters.map((chapter: ChapterListProps) => (
|
||||
<option key={chapter.chapterId} value={chapter.chapterId}>
|
||||
{`${chapter.chapterOrder}. ${chapter.title}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={() => onLinkChapter(actId, selectedChapterId, item.plotPointId)}
|
||||
disabled={!selectedChapterId}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noPlotPointAdded')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200 placeholder:text-muted/60"
|
||||
value={newPlotPointTitle}
|
||||
onChange={(e) => setNewPlotPointTitle(e.target.value)}
|
||||
placeholder={t('newPlotPointPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={``}
|
||||
onChangeCallBack={(e) => setSelectedIncidentId(e.target.value)}
|
||||
data={getIncidentData()}
|
||||
/>
|
||||
}
|
||||
addButtonCallBack={(): Promise<void> => onAddPlotPoint(actId)}
|
||||
isAddButtonDisabled={newPlotPointTitle.trim() === ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
132
components/book/settings/world/WorldElement.tsx
Normal file
132
components/book/settings/world/WorldElement.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import {ChangeEvent, useContext, useState} from "react";
|
||||
import {WorldContext} from "@/context/WorldContext";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import {WorldElement, WorldProps} from "@/lib/models/World";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import System from "@/lib/models/System";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface WorldElementInputProps {
|
||||
sectionLabel: string;
|
||||
sectionType: string;
|
||||
}
|
||||
|
||||
export default function WorldElementComponent({sectionLabel, sectionType}: WorldElementInputProps) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {worlds, setWorlds, selectedWorldIndex} = useContext(WorldContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
|
||||
const [newElementName, setNewElementName] = useState<string>('');
|
||||
|
||||
async function handleRemoveElement(
|
||||
section: keyof WorldProps,
|
||||
index: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('book/world/element/delete', {
|
||||
elementId: (worlds[selectedWorldIndex][section] as WorldElement[])[index].id,
|
||||
}, session.accessToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
}
|
||||
const updatedWorlds: WorldProps[] = [...worlds];
|
||||
(updatedWorlds[selectedWorldIndex][section] as WorldElement[]).splice(
|
||||
index,
|
||||
1,
|
||||
);
|
||||
setWorlds(updatedWorlds);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.toString());
|
||||
} else {
|
||||
errorMessage(t("worldElementComponent.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddElement(section: keyof WorldProps): Promise<void> {
|
||||
if (newElementName.trim() === '') {
|
||||
errorMessage(t("worldElementComponent.emptyField", {section: sectionLabel}));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const elementId: string = await System.authPostToServer('book/world/element/add', {
|
||||
elementType: section,
|
||||
worldId: worlds[selectedWorldIndex].id,
|
||||
elementName: newElementName,
|
||||
}, session.accessToken, lang);
|
||||
if (!elementId) {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
return;
|
||||
}
|
||||
const updatedWorlds: WorldProps[] = [...worlds];
|
||||
(updatedWorlds[selectedWorldIndex][section] as WorldElement[]).push({
|
||||
id: elementId,
|
||||
name: newElementName,
|
||||
description: '',
|
||||
});
|
||||
setWorlds(updatedWorlds);
|
||||
setNewElementName('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("worldElementComponent.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleElementChange(
|
||||
section: keyof WorldProps,
|
||||
index: number,
|
||||
field: keyof WorldElement,
|
||||
value: string,
|
||||
): void {
|
||||
const updatedWorlds: WorldProps[] = [...worlds];
|
||||
const sectionElements = updatedWorlds[selectedWorldIndex][
|
||||
section
|
||||
] as WorldElement[];
|
||||
sectionElements[index] = {...sectionElements[index], [field]: value};
|
||||
setWorlds(updatedWorlds);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Array.isArray(worlds[selectedWorldIndex][sectionType as keyof WorldProps]) &&
|
||||
(worlds[selectedWorldIndex][sectionType as keyof WorldProps] as WorldElement[]).map(
|
||||
(element: WorldElement, index: number) => (
|
||||
<div key={element.id}
|
||||
className="bg-secondary/30 rounded-xl p-4 border-l-4 border-primary shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<div className="mb-2">
|
||||
<InputField input={<TextInput
|
||||
value={element.name}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => handleElementChange(sectionType as keyof WorldProps, index, 'name', e.target.value)}
|
||||
placeholder={t("worldElementComponent.namePlaceholder", {section: sectionLabel.toLowerCase()})}
|
||||
/>}
|
||||
removeButtonCallBack={(): Promise<void> => handleRemoveElement(sectionType as keyof WorldProps, index)}/>
|
||||
</div>
|
||||
<TexteAreaInput
|
||||
value={element.description}
|
||||
setValue={(e) => handleElementChange(sectionType as keyof WorldProps, index, 'description', e.target.value)}
|
||||
placeholder={t("worldElementComponent.descriptionPlaceholder", {section: sectionLabel.toLowerCase()})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
<InputField input={<TextInput
|
||||
value={newElementName}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>): void => setNewElementName(e.target.value)}
|
||||
placeholder={t("worldElementComponent.newPlaceholder", {section: sectionLabel.toLowerCase()})}
|
||||
/>} addButtonCallBack={(): Promise<void> => handleAddElement(sectionType as keyof WorldProps)}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
components/book/settings/world/WorldSetting.tsx
Normal file
309
components/book/settings/world/WorldSetting.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
'use client'
|
||||
import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faPlus, IconDefinition} from "@fortawesome/free-solid-svg-icons";
|
||||
import {WorldContext} from '@/context/WorldContext';
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import System from "@/lib/models/System";
|
||||
import {elementSections, WorldProps} from "@/lib/models/World";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TextInput from '@/components/form/TextInput';
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import WorldElementComponent from './WorldElement';
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
export interface ElementSection {
|
||||
title: string;
|
||||
section: keyof WorldProps;
|
||||
icon: IconDefinition;
|
||||
}
|
||||
|
||||
export function WorldSetting(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const bookId: string = book?.bookId ? book.bookId.toString() : '';
|
||||
|
||||
const [worlds, setWorlds] = useState<WorldProps[]>([]);
|
||||
const [newWorldName, setNewWorldName] = useState<string>('');
|
||||
const [selectedWorldIndex, setSelectedWorldIndex] = useState<number>(0);
|
||||
const [worldsSelector, setWorldsSelector] = useState<SelectBoxProps[]>([]);
|
||||
const [showAddNewWorld, setShowAddNewWorld] = useState<boolean>(false);
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleUpdateWorld,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getWorlds().then();
|
||||
}, []);
|
||||
|
||||
async function getWorlds() {
|
||||
try {
|
||||
const response: WorldProps[] = await System.authGetQueryToServer<WorldProps[]>(`book/worlds`, session.accessToken, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
if (response) {
|
||||
setWorlds(response);
|
||||
const formattedWorlds: SelectBoxProps[] = response.map(
|
||||
(world: WorldProps): SelectBoxProps => ({
|
||||
label: world.name,
|
||||
value: world.id.toString(),
|
||||
}),
|
||||
);
|
||||
setWorldsSelector(formattedWorlds);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddNewWorld(): Promise<void> {
|
||||
if (newWorldName.trim() === '') {
|
||||
errorMessage(t("worldSetting.newWorldNameError"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const worldId: string = await System.authPostToServer<string>('book/world/add', {
|
||||
worldName: newWorldName,
|
||||
bookId: bookId,
|
||||
}, session.accessToken, lang);
|
||||
if (!worldId) {
|
||||
errorMessage(t("worldSetting.addWorldError"));
|
||||
return;
|
||||
}
|
||||
const newWorldId: string = worldId;
|
||||
const newWorld: WorldProps = {
|
||||
id: newWorldId,
|
||||
name: newWorldName,
|
||||
history: '',
|
||||
politics: '',
|
||||
economy: '',
|
||||
religion: '',
|
||||
languages: '',
|
||||
laws: [],
|
||||
biomes: [],
|
||||
issues: [],
|
||||
customs: [],
|
||||
kingdoms: [],
|
||||
climate: [],
|
||||
resources: [],
|
||||
wildlife: [],
|
||||
arts: [],
|
||||
ethnicGroups: [],
|
||||
socialClasses: [],
|
||||
importantCharacters: [],
|
||||
};
|
||||
setWorlds([...worlds, newWorld]);
|
||||
setWorldsSelector([
|
||||
...worldsSelector,
|
||||
{label: newWorldName, value: newWorldId.toString()},
|
||||
]);
|
||||
setNewWorldName('');
|
||||
setShowAddNewWorld(false);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateWorld(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPutToServer<boolean>('book/world/update', {
|
||||
world: worlds[selectedWorldIndex],
|
||||
bookId: bookId,
|
||||
}, session.accessToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("worldSetting.updateWorldError"));
|
||||
return;
|
||||
}
|
||||
successMessage(t("worldSetting.updateWorldSuccess"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("worldSetting.unknownError"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputChange(value: string, field: keyof WorldProps) {
|
||||
const updatedWorlds = [...worlds] as WorldProps[];
|
||||
(updatedWorlds[selectedWorldIndex][field] as string) = value;
|
||||
setWorlds(updatedWorlds);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-lg">
|
||||
<div className="grid grid-cols-1 gap-4 mb-4">
|
||||
<InputField
|
||||
fieldName={t("worldSetting.selectWorld")}
|
||||
input={
|
||||
<SelectBox
|
||||
onChangeCallBack={(e) => {
|
||||
const worldId = e.target.value;
|
||||
const index = worlds.findIndex(world => world.id.toString() === worldId);
|
||||
if (index !== -1) {
|
||||
setSelectedWorldIndex(index);
|
||||
}
|
||||
}}
|
||||
data={worldsSelector.length > 0 ? worldsSelector : [{
|
||||
label: t("worldSetting.noWorldAvailable"),
|
||||
value: '0'
|
||||
}]}
|
||||
defaultValue={worlds[selectedWorldIndex]?.id.toString() || '0'}
|
||||
placeholder={t("worldSetting.selectWorldPlaceholder")}
|
||||
/>
|
||||
}
|
||||
actionIcon={faPlus}
|
||||
actionLabel={t("worldSetting.addWorldLabel")}
|
||||
action={async () => setShowAddNewWorld(!showAddNewWorld)}
|
||||
/>
|
||||
|
||||
{showAddNewWorld && (
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={newWorldName}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => setNewWorldName(e.target.value)}
|
||||
placeholder={t("worldSetting.newWorldPlaceholder")}
|
||||
/>
|
||||
}
|
||||
actionIcon={faPlus}
|
||||
actionLabel={t("worldSetting.createWorldLabel")}
|
||||
addButtonCallBack={handleAddNewWorld}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{worlds.length > 0 && worlds[selectedWorldIndex] ? (
|
||||
<WorldContext.Provider value={{worlds, setWorlds, selectedWorldIndex}}>
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-lg">
|
||||
<div className="mb-4">
|
||||
<InputField
|
||||
fieldName={t("worldSetting.worldName")}
|
||||
input={
|
||||
<TextInput
|
||||
value={worlds[selectedWorldIndex].name}
|
||||
setValue={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedWorlds: WorldProps[] = [...worlds];
|
||||
updatedWorlds[selectedWorldIndex].name = e.target.value
|
||||
setWorlds(updatedWorlds);
|
||||
}}
|
||||
placeholder={t("worldSetting.worldNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
fieldName={t("worldSetting.worldHistory")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={worlds[selectedWorldIndex].history || ''}
|
||||
setValue={(e) => handleInputChange(e.target.value, 'history')}
|
||||
placeholder={t("worldSetting.worldHistoryPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-4 border border-secondary/50">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<InputField
|
||||
fieldName={t("worldSetting.politics")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={worlds[selectedWorldIndex].politics || ''}
|
||||
setValue={(e) => handleInputChange(e.target.value, 'politics')}
|
||||
placeholder={t("worldSetting.politicsPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputField
|
||||
fieldName={t("worldSetting.economy")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={worlds[selectedWorldIndex].economy || ''}
|
||||
setValue={(e) => handleInputChange(e.target.value, 'economy')}
|
||||
placeholder={t("worldSetting.economyPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-4 border border-secondary/50">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<InputField
|
||||
fieldName={t("worldSetting.religion")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={worlds[selectedWorldIndex].religion || ''}
|
||||
setValue={(e) => handleInputChange(e.target.value, 'religion')}
|
||||
placeholder={t("worldSetting.religionPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputField
|
||||
fieldName={t("worldSetting.languages")}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={worlds[selectedWorldIndex].languages || ''}
|
||||
setValue={(e) => handleInputChange(e.target.value, 'languages')}
|
||||
placeholder={t("worldSetting.languagesPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{elementSections.map((section, index) => (
|
||||
<div key={index}
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-4 border border-secondary/50">
|
||||
<h3 className="text-lg font-semibold text-text-primary mb-4 flex items-center">
|
||||
<FontAwesomeIcon icon={section.icon} className="mr-2 w-5 h-5"/>
|
||||
{section.title}
|
||||
<span
|
||||
className="ml-2 text-sm bg-dark-background text-text-secondary py-0.5 px-2 rounded-full">
|
||||
{worlds[selectedWorldIndex][section.section]?.length || 0}
|
||||
</span>
|
||||
</h3>
|
||||
<WorldElementComponent
|
||||
sectionLabel={section.title}
|
||||
sectionType={section.section}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</WorldContext.Provider>
|
||||
) : (
|
||||
<div
|
||||
className="bg-tertiary/90 backdrop-blur-sm rounded-xl shadow-lg p-8 border border-secondary/50 text-center">
|
||||
<p className="text-text-secondary mb-4">{t("worldSetting.noWorldAvailable")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(WorldSetting);
|
||||
Reference in New Issue
Block a user