summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/Dialog.tsx54
-rw-r--r--frontend/src/components/ElementCreator.tsx203
-rw-r--r--frontend/src/components/ElementList.tsx76
-rw-r--r--frontend/src/components/ElementView.tsx58
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>
);