'use client' import {MapPin, Plus, Share2, ToggleRight, Trash2} from 'lucide-react'; import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react'; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {BookContext, BookContextProps} from "@/context/BookContext"; import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client'; import InputField from "@/components/form/InputField"; import TextInput from '@/components/form/TextInput'; import TextAreaInput from "@/components/form/TextAreaInput"; import {useTranslations} from '@/lib/i18n'; import {LangContext, LangContextProps} from "@/context/LangContext"; import ToggleSwitch from "@/components/form/ToggleSwitch"; import {SeriesLocationElement, SeriesLocationItem, SeriesLocationSubElement} from "@/lib/types/series"; import SeriesImportSelector from "@/components/form/SeriesImportSelector"; import IconButton from "@/components/ui/IconButton"; 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[]; seriesLocationId?: string | null; } interface LocationListResponse { locations: LocationProps[]; enabled: boolean; } interface LocationComponentProps { showToggle?: boolean; entityType?: 'book' | 'series'; entityId?: string; } export function LocationComponent(props: LocationComponentProps, ref: React.Ref<{ handleSave: () => Promise }>) { const {showToggle = true, entityType = 'book', entityId} = props; const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {successMessage, errorMessage}: AlertContextProps = useContext(AlertContext); const {book, setBook}: BookContextProps = useContext(BookContext); const currentEntityId: string = entityId || book?.bookId || ''; const isSeriesMode: boolean = entityType === 'series'; const token: string = session.accessToken; const [sections, setSections] = useState([]); const [seriesLocations, setSeriesLocations] = useState([]); const [newSectionName, setNewSectionName] = useState(''); const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({}); const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({}); const [toolEnabled, setToolEnabled] = useState(isSeriesMode ? true : (book?.tools?.locations ?? false)); const bookSeriesId: string | null = book?.seriesId || null; useImperativeHandle(ref, function () { return { handleSave: handleSave, }; }); useEffect((): void => { if (currentEntityId) { getAllLocations().then(); } }, [currentEntityId]); useEffect((): void => { if (bookSeriesId && !isSeriesMode) { getSeriesLocations().then(); } }, [bookSeriesId]); async function getSeriesLocations(): Promise { if (!bookSeriesId) return; try { const response: SeriesLocationItem[] = await apiGet( 'series/location/list', token, lang, {seriesid: bookSeriesId} ); if (response) { setSeriesLocations(response); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function handleToggleTool(enabled: boolean): Promise { if (isSeriesMode) return; try { const response: boolean = await apiPatch('book/tool-setting', { bookId: currentEntityId, toolName: 'locations', enabled: enabled }, token, lang); if (response && setBook && book) { setToolEnabled(enabled); setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: book.tools?.worlds ?? false, locations: enabled, spells: book.tools?.spells ?? false } }); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function getAllLocations(): Promise { try { if (isSeriesMode) { const response: SeriesLocationItem[] = await apiGet( 'series/location/list', token, lang, {seriesid: currentEntityId} ); if (response) { const mappedLocations: LocationProps[] = response.map((loc: SeriesLocationItem): LocationProps => ({ id: loc.id, name: loc.name, elements: loc.elements.map((elem: SeriesLocationElement) => ({ id: elem.id, name: elem.name, description: elem.description, subElements: elem.subElements.map((sub: SeriesLocationSubElement) => ({ id: sub.id, name: sub.name, description: sub.description, })), })), })); setSections(mappedLocations); } } else { const response: LocationListResponse = await apiGet( 'location/all', token, lang, {bookid: currentEntityId} ); if (response) { setSections(response.locations); setToolEnabled(response.enabled); if (setBook && book) { setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: book.tools?.worlds ?? false, locations: response.enabled, spells: book.tools?.spells ?? false } }); } } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t('locationComponent.errorUnknownFetchLocations')); } } } async function handleAddSection(): Promise { if (!newSectionName.trim()) { errorMessage(t('locationComponent.errorSectionNameEmpty')) return } try { let sectionId: string; if (isSeriesMode) { sectionId = await apiPost( 'series/location/section/add', { seriesId: currentEntityId, name: newSectionName, }, token, lang ); if (!sectionId) { errorMessage(t('locationComponent.errorUnknownAddSection')); return; } } else { sectionId = await apiPost('location/section/add', { bookId: currentEntityId, 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 { if (!newElementNames[sectionId]?.trim()) { errorMessage(t('locationComponent.errorElementNameEmpty')) return } try { let elementId: string; if (isSeriesMode) { elementId = await apiPost( 'series/location/element/add', { locationId: sectionId, name: newElementNames[sectionId], }, token, lang ); if (!elementId) { errorMessage(t('locationComponent.errorUnknownAddElement')); return; } } else { elementId = await apiPost('location/element/add', { bookId: currentEntityId, 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 { if (!newSubElementNames[elementIndex]?.trim()) { errorMessage(t('locationComponent.errorSubElementNameEmpty')) return } const sectionIndex: number = sections.findIndex( (section: LocationProps): boolean => section.id === sectionId, ); try { let subElementId: string; if (isSeriesMode) { subElementId = await apiPost( 'series/location/sub-element/add', { elementId: sections[sectionIndex].elements[elementIndex].id, name: newSubElementNames[elementIndex], }, token, lang ); if (!subElementId) { errorMessage(t('locationComponent.errorUnknownAddSubElement')); return; } } else { subElementId = await apiPost('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 { try { const elementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) ?.elements[elementIndex].id; let success: boolean; if (isSeriesMode) { success = await apiDelete('series/location/element/delete', { elementId: elementId }, token, lang); } else { success = await apiDelete('location/element/delete', { elementId: elementId, }, token, lang); } if (!success) { 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 { try { const subElementId: string | undefined = sections.find((section: LocationProps): boolean => section.id === sectionId) ?.elements[elementIndex].subElements[subElementIndex].id; let success: boolean; if (isSeriesMode) { success = await apiDelete('series/location/sub-element/delete', { subElementId: subElementId }, token, lang); } else { success = await apiDelete('location/sub-element/delete', { subElementId: subElementId, }, token, lang); } if (!success) { 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 { try { let success: boolean; if (isSeriesMode) { success = await apiDelete('series/location/delete', { locationId: sectionId }, token, lang); } else { success = await apiDelete('location/delete', { locationId: sectionId, }, token, lang); } if (!success) { 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 { try { const response: boolean = await apiPost(`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')); } } } async function handleExportToSeries(section: LocationProps): Promise { if (!bookSeriesId) return; try { const seriesLocationId: string = await apiPost('series/location/section/add', { seriesId: bookSeriesId, name: section.name, }, token, lang); if (seriesLocationId) { const updateResponse: boolean = await apiPost('location/section/update', { sectionId: section.id, sectionName: section.name, seriesLocationId: seriesLocationId, }, token, lang); if (updateResponse) { setSections(sections.map((s: LocationProps): LocationProps => s.id === section.id ? {...s, seriesLocationId: seriesLocationId} : s )); await getSeriesLocations(); successMessage(t("locationComponent.exportSuccess")); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function handleImportFromSeries(seriesLocationId: string): Promise { const seriesLocation: SeriesLocationItem | undefined = seriesLocations.find((location: SeriesLocationItem): boolean => location.id === seriesLocationId); if (!seriesLocation) return; try { const sectionId: string = await apiPost('location/section/add', { bookId: currentEntityId, locationName: seriesLocation.name, seriesLocationId: seriesLocationId, }, token, lang); if (!sectionId) { errorMessage(t('locationComponent.importError')); return; } const importedElements: Element[] = []; for (const seriesElement of seriesLocation.elements) { const elementId: string = await apiPost('location/element/add', { bookId: currentEntityId, locationId: sectionId, elementName: seriesElement.name, }, token, lang); if (!elementId) continue; const importedSubElements: SubElement[] = []; for (const seriesSubElement of seriesElement.subElements) { const subElementId: string = await apiPost('location/sub-element/add', { elementId: elementId, subElementName: seriesSubElement.name, }, token, lang); if (subElementId) { importedSubElements.push({ id: subElementId, name: seriesSubElement.name, description: seriesSubElement.description, }); } } importedElements.push({ id: elementId, name: seriesElement.name, description: seriesElement.description, subElements: importedSubElements, }); } const newLocation: LocationProps = { id: sectionId, name: seriesLocation.name, elements: importedElements, seriesLocationId: seriesLocationId, }; setSections([...sections, newLocation]); successMessage(t('locationComponent.importSuccess')); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } return (
{showToggle && !isSeriesMode && (
=> handleToggleTool(checked)} /> } />

{t('locationComponent.enableToolDescription')}

)} {(toolEnabled || isSeriesMode) && ( <> {!isSeriesMode && bookSeriesId && seriesLocations.filter((seriesLocation: SeriesLocationItem): boolean => !sections.some((section: LocationProps): boolean => section.seriesLocationId === seriesLocation.id)).length > 0 && ( !sections.some((section: LocationProps): boolean => section.seriesLocationId === seriesLocation.id)) .map((seriesLocation: SeriesLocationItem) => ({ id: seriesLocation.id, name: seriesLocation.name }))} onImport={handleImportFromSeries} placeholder={t("seriesImport.selectElement")} label={t("seriesImport.importFromSeries")} /> )}
) => setNewSectionName(e.target.value)} placeholder={t("locationComponent.newSectionPlaceholder")} /> } actionIcon={Plus} actionLabel={t("locationComponent.addSectionLabel")} addButtonCallBack={handleAddSection} />
{sections.length > 0 ? ( sections.map((section: LocationProps) => (

{section.name} {section.elements.length || 0}
{!isSeriesMode && bookSeriesId && !section.seriesLocationId && ( => handleExportToSeries(section)}/> )} => handleRemoveSection(section.id)}/>

{section.elements.length > 0 ? ( section.elements.map((element, elementIndex) => (
) => handleElementChange(section.id, elementIndex, 'name', e.target.value) } placeholder={t("locationComponent.elementNamePlaceholder")} /> } removeButtonCallBack={(): Promise => handleRemoveElement(section.id, elementIndex)} />
): void => handleElementChange(section.id, elementIndex, 'description', e.target.value)} placeholder={t("locationComponent.elementDescriptionPlaceholder")} />
{element.subElements.length > 0 && (

{t("locationComponent.subElementsHeading")}

)} {element.subElements.map((subElement: SubElement, subElementIndex: number) => (
): void => handleSubElementChange(section.id, elementIndex, subElementIndex, 'name', e.target.value) } placeholder={t("locationComponent.subElementNamePlaceholder")} /> } removeButtonCallBack={(): Promise => handleRemoveSubElement(section.id, elementIndex, subElementIndex)} />
handleSubElementChange(section.id, elementIndex, subElementIndex, 'description', e.target.value) } placeholder={t("locationComponent.subElementDescriptionPlaceholder")} />
))} ) => setNewSubElementNames({ ...newSubElementNames, [elementIndex]: e.target.value }) } placeholder={t("locationComponent.newSubElementPlaceholder")} /> } addButtonCallBack={(): Promise => handleAddSubElement(section.id, elementIndex)} />
)) ) : (
{t("locationComponent.noElementAvailable")}
)} ) => setNewElementNames({ ...newElementNames, [section.id]: e.target.value }) } placeholder={t("locationComponent.newElementPlaceholder")} /> } addButtonCallBack={(): Promise => handleAddElement(section.id)} />
)) ) : (

{t("locationComponent.noSectionAvailable")}

)} )}
); } export default forwardRef(LocationComponent);