diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/Dialog.tsx | 54 | ||||
| -rw-r--r-- | frontend/src/components/ElementCreator.tsx | 203 | ||||
| -rw-r--r-- | frontend/src/components/ElementList.tsx | 76 | ||||
| -rw-r--r-- | frontend/src/components/ElementView.tsx | 58 |
4 files changed, 377 insertions, 14 deletions
diff --git a/frontend/src/components/Dialog.tsx b/frontend/src/components/Dialog.tsx new file mode 100644 index 0000000..4fa6bf7 --- /dev/null +++ b/frontend/src/components/Dialog.tsx @@ -0,0 +1,54 @@ +import { FC, PropsWithChildren, useEffect, useRef } from 'react'; + +interface DialogProps { + visible: boolean; + closeDialog: () => void; +} + +const Dialog: FC<PropsWithChildren<DialogProps>> = ({ + visible, + closeDialog, + children, +}) => { + const dialogRef = useRef<HTMLDialogElement>(null); + + useEffect(() => { + if (visible) { + dialogRef.current?.showModal(); + } else { + dialogRef.current?.close(); + } + }, [visible]); + + const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + closeDialog(); + }; + + return ( + <dialog ref={dialogRef}> + <div className="p-4 flex flex-col max-w-[90vw] md:max-w-[50vw] bg-white shadow"> + <div className="flex w-full"> + <button className="ml-auto" onClick={handleButtonClick}> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-6 h-6"> + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M6 18 18 6M6 6l12 12" + /> + </svg> + </button> + </div> + {children} + </div> + </dialog> + ); +}; + +export default Dialog; diff --git a/frontend/src/components/ElementCreator.tsx b/frontend/src/components/ElementCreator.tsx new file mode 100644 index 0000000..61f5aba --- /dev/null +++ b/frontend/src/components/ElementCreator.tsx @@ -0,0 +1,203 @@ +import { FC, useEffect, useState } from 'react'; +import { + Element, + Color, + ColorBitmap, + ElementState, + ElementSuggestion, + ColorCode, +} from '../types'; +import toast from 'react-hot-toast'; +import ElementView from './ElementView'; +import { userStore } from '../stores'; + +interface ElementCreatorProps { + firstElementId: number | undefined; + secondElementId: number | undefined; + closeDialog: () => void; +} + +const ElementCreator: FC<ElementCreatorProps> = ({ + firstElementId, + secondElementId, + closeDialog, +}) => { + const [elementName, setElementName] = useState<string>(''); + const [color, setColor] = useState<Color>(Color.Air); + const [firstElement, setFirstElement] = useState<Element | null>(null); + const [secondElement, setSecondElement] = useState<Element | null>(null); + const setUser = userStore((store) => store.setUser); + + const fetchElement = async (elementId: number): Promise<Element> => { + const elementResponse = await fetch(`/api/element/${elementId}`); + return await elementResponse.json(); + }; + + useEffect(() => { + if (firstElementId == undefined || secondElementId == undefined) { + return; + } + + setElementName(''); + setColor(Color.Air); + + fetchElement(firstElementId).then((elem) => { + setFirstElement(elem); + }); + fetchElement(secondElementId).then((elem) => { + setSecondElement(elem); + }); + }, [firstElementId, secondElementId]); + + const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setElementName(e.target.value); + }; + + const createHandleColorClick = (color: Color) => { + return (e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + setColor(color); + }; + }; + + const handleSubmitButton = async (e: React.MouseEvent<HTMLButtonElement>) => { + e.preventDefault(); + + if (elementName == '') { + toast.error('No name selected'); + return; + } + if (!(color in Color)) { + toast.error('Bad color value'); + return; + } + + const request: ElementSuggestion = { + name: elementName, + iconBitmap: ColorBitmap[color], + firstElementId: firstElementId!, + secondElementId: secondElementId!, + }; + + console.log(request); + + const createVoteResponse = await fetch(`/api/suggestion/create`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (createVoteResponse.status == 401) { + console.log('Session expired'); + toast.error('Your authorization has expired. You have to log in again.'); + localStorage.removeItem('user'); + setUser(undefined); + return; + } + console.log(createVoteResponse.status); + console.log('Vote started'); + closeDialog(); + }; + + return ( + <form className="flex flex-col items-center"> + <div className="flex items-center"> + {firstElement != null && secondElement != null && ( + <> + <ElementView + element={firstElement!} + onElementCombine={() => {}} + isPreview={false} + interactive={false} + /> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-6 h-6"> + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M12 4.5v15m7.5-7.5h-15" + /> + </svg> + <ElementView + element={secondElement!} + onElementCombine={() => {}} + isPreview={false} + interactive={false} + /> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-6 h-6"> + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M3.75 9h16.5m-16.5 6.75h16.5" + /> + </svg> + </> + )} + <ElementView + element={{ + name: elementName, + icon: ColorCode[color], + id: 0, + userId: 0, + state: ElementState.HasColor, + }} + onElementCombine={() => {}} + isPreview={true} + interactive={false} + /> + </div> + <div className="mb-5 w-full"> + <label + htmlFor="element-name" + className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"> + Element name + </label> + <input + type="text" + id="element-name" + value={elementName} + onChange={handleNameChange} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + required + /> + </div> + <div className="flex flex-wrap mb-3 gap-2 border p-2 rounded shadow justify-center"> + {(Object.keys(Color) as Array<keyof typeof Color>) + .filter((color) => Number.isNaN(Number(color))) + .map((color) => { + return ( + <button + key={color} + onClick={createHandleColorClick(Color[color])}> + <div + className="border border-gray-200 w-8 h-8" + style={{ backgroundColor: ColorCode[Color[color]] }}></div> + </button> + ); + })} + </div> + <button + onClick={handleSubmitButton} + type="submit" + className=" text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> + Submit + </button> + </form> + ); +}; + +export default ElementCreator; diff --git a/frontend/src/components/ElementList.tsx b/frontend/src/components/ElementList.tsx index 20b27dd..04650c4 100644 --- a/frontend/src/components/ElementList.tsx +++ b/frontend/src/components/ElementList.tsx @@ -1,22 +1,35 @@ import { FC, useEffect, useState } from 'react'; -import {Element} from '../types'; +import { Element } from '../types'; import ElementView from './ElementView'; +import Dialog from './Dialog'; +import ElementCreator from './ElementCreator'; +import { userStore } from '../stores'; +import toast from 'react-hot-toast'; const ElementList: FC = () => { const [elements, setElements] = useState<Element[]>([]); + const [elementCreatorVisible, setElementCreatorVisible] = + useState<boolean>(false); + const [creatorFirstElementId, setCreatorFirstElementId] = useState< + number | undefined + >(undefined); + const [creatorSecondElementId, setCreatorSecondElementId] = useState< + number | undefined + >(undefined); + const user = userStore((store) => store.user); useEffect(() => { const initialElements: number[] = [1, 2, 3, 4]; const elementStateString = localStorage.getItem('elementState'); if (elementStateString != null) { - setElements(JSON.parse(elementStateString)); - return; + setElements(JSON.parse(elementStateString)); + return; } const fetchElements = async () => { const elems: Element[] = []; - + for (const elemId of initialElements) { const response = await fetch(`/api/element/${elemId}`); const elem: Element = await response.json(); @@ -32,14 +45,59 @@ const ElementList: FC = () => { useEffect(() => { if (elements.length != 0) { - localStorage.setItem('elementState', JSON.stringify(elements)); + localStorage.setItem('elementState', JSON.stringify(elements)); } - }, [elements]) + }, [elements]); + + const createOnElementCombine = (firstElementId: number) => { + return async (secondElementId: number) => { + const combineResponse = await fetch( + `/api/element/combine?firstElementId=${firstElementId}&secondElementId=${secondElementId}`, + ); + if (combineResponse.status == 404) { + console.log("Element doesn't exist"); + if (!user) { + toast("You have discovered a new element but need to log in to name it"); + return; + } + setCreatorFirstElementId(firstElementId); + setCreatorSecondElementId(secondElementId); + setElementCreatorVisible(true); + return; + } + + const newElem: Element = await combineResponse.json(); + if (elements.find((e) => e.id == newElem.id)) { + toast(`You have already discovered ${newElem.name}`); + return; + } + setElements((elems) => elems.concat(newElem)); + }; + }; return ( - <> - {elements.map(elem => <ElementView key={elem.id} element={elem} />)} - </> + <div className="p-3 flex gap-3"> + {elements.map((elem) => ( + <ElementView + key={elem.id} + interactive={true} + isPreview={false} + element={elem} + onElementCombine={createOnElementCombine(elem.id)} + /> + ))} + <Dialog + visible={elementCreatorVisible} + closeDialog={() => { + setElementCreatorVisible(false); + }}> + <ElementCreator + firstElementId={creatorFirstElementId} + secondElementId={creatorSecondElementId} + closeDialog={() => setElementCreatorVisible(false)} + /> + </Dialog> + </div> ); }; diff --git a/frontend/src/components/ElementView.tsx b/frontend/src/components/ElementView.tsx index 4f9d489..1b11ffb 100644 --- a/frontend/src/components/ElementView.tsx +++ b/frontend/src/components/ElementView.tsx @@ -2,15 +2,63 @@ import { FC } from 'react'; import { Element } from '../types'; interface ElementViewProps { + onElementCombine: (elementId: number) => void; element: Element; + interactive: boolean; + isPreview: boolean; } -const ElementView: FC<ElementViewProps> = ({ element }) => { +const ElementView: FC<ElementViewProps> = ({ + onElementCombine, + element, + interactive, + isPreview, +}) => { + const onDragStart = (e: React.DragEvent<HTMLDivElement>) => { + e.stopPropagation(); + + e.dataTransfer.setData('text/plain', element.name); + e.dataTransfer.setData('id', element.id.toString()); + }; + + const onDragOver = (e: React.DragEvent<HTMLDivElement>) => { + e.preventDefault(); + }; + + const onDrop = (e: React.DragEvent<HTMLDivElement>) => { + const elementId = e.dataTransfer.getData('id'); + onElementCombine(Number.parseInt(elementId)); + e.preventDefault(); + }; + return ( - <div className='flex flex-row m-2 rounded-md border border-gray-300 bg-gray-100 w-fit h-fit'> - <div className='flex flex-col items-center'> - <img src={`data:image/png;base64,${element.icon}`} width='80px' height='80px'/> - <p className='my-1 mx-2 text-sm'>{element.name}</p> + <div + draggable={interactive} + onDragStart={interactive ? onDragStart : undefined} + onDragOver={interactive ? onDragOver : undefined} + onDrop={interactive ? onDrop : undefined} + className="element flex flex-row rounded-md border border-gray-300 bg-gray-100 w-[96px] h-fit"> + <div draggable={false} className="flex flex-col items-center w-full"> + {isPreview ? ( + <div + style={{ + backgroundColor: element.icon, + }} + className="rounded-t w-full h-[96px]"></div> + ) : ( + <img + draggable={false} + className="rounded-t" + src={`data:image/png;base64,${element.icon}`} + width="96px" + height="96px" + /> + )} + <span + draggable={false} + className="my-1 mx-2 text-sm leading-tight text-center w-full"> + {element.name.length > 0 ? element.name : '\u00A0'} + </span> </div> </div> ); |
