Add components for Act management and integrate Electron setup
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user