initial commit

This commit is contained in:
Sebastian Dine
2022-12-02 14:37:07 +00:00
commit 28579efc80
74 changed files with 8978 additions and 0 deletions
@@ -0,0 +1,65 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/magic";
import { CreateEditModalTemplate } from "../templates";
// Enum to control wether the modal is in "Create" or "Edit" mode.
export enum Mode {
Create,
Edit,
}
/**
* Modal to create a new Magic card entry or edit an existing one.
* The modal needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
*
* * mode - Flag to specifiy if the modal should be in "Create new Entry" or "Edit existing Entry" node.
*
* * collection - Reference to the current Magic `CardEntry` collection.
* * setCollection - Function to set/update the collection list to add new entries or update existing ones.
*
* * selectedEntry - Reference to the currently selected `CardEntry` entry that should be edited in case the modal is in `Edit` mode.
* * setSelectedEntry - Function to set/update the currently selected `CardEntry` entry.
*
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
*/
const CreateEditMtgModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
collection: CardEntry[];
mode: Mode;
selectedEntry: CardEntry;
setCollection: Dispatch<SetStateAction<CardEntry[]>>;
setSelectedEntry: Dispatch<SetStateAction<CardEntry>>;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const extraAttributes = [{ label: "Foil", accessKey: "foil" }];
return (
<CreateEditModalTemplate
visible={props.visible}
setVisible={props.setVisible}
game="Magic"
extraAttributes={extraAttributes}
selectedEntry={props.selectedEntry}
setSelectedEntry={props.setSelectedEntry}
mode={props.mode}
collection={props.collection}
setCollection={props.setCollection}
setImageModalImages={props.setImageModalImages}
setImageModalImageIndex={props.setImageModalImageIndex}
setImageModalVisible={props.setImageModalVisible}
/>
);
};
export default CreateEditMtgModal;
@@ -0,0 +1,47 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/magic";
import { TableTemplate } from "../templates";
import { IoSparklesSharp } from "react-icons/io5";
import { BsPencilFill, BsPaletteFill } from "react-icons/bs";
/**
* Table to display a collection of Magic cards and select entries from it.
*
* # Props:
* * collection - List of `CardEntry` objects that should be displayed via the table.
* * selectedEntry - `CardEntry` object that is currently selected by the user.
* * setCollection - Function to set/update the collection list that should be displayed.
* * setSelectedEntry - Function to specifiy, which entry from the table the user has currenty selected.
*/
const MtgTable: React.FC<{
collection: CardEntry[];
selectedEntry: CardEntry;
setCollection: Dispatch<SetStateAction<CardEntry[]>>;
setSelectedEntry: Dispatch<SetStateAction<CardEntry>>;
}> = (props) => {
const tableFields = [
{label: "Name", valueKey: "name", sortKey: "name"},
{label: "Set", valueKey: "set.name", sortKey: "set.releaseDate"},
{label: "Language", valueKey: "language", sortKey: "language"},
{label: "Condition", valueKey: "condition", sortKey: "condition"},
{label: "#", valueKey: "amount", sortKey: "amount"},
{label: "Foil", valueKey: "foil", sortKey: "foil", icon: <IoSparklesSharp /> },
{label: "Signed", valueKey: "signed", sortKey: "signed", icon: <BsPencilFill /> },
{label: "Altered", valueKey: "altered", sortKey: "altered", icon: <BsPaletteFill /> },
{label: "Note", valueKey: "note", sortKey: "note"}
];
return (
<TableTemplate
tableFields={tableFields}
collection={props.collection}
selectedEntry={props.selectedEntry}
setCollection={props.setCollection}
setSelectedEntry={props.setSelectedEntry}
/>
);
};
export default MtgTable;
@@ -0,0 +1,61 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/magic";
import { IoSparklesSharp } from "react-icons/io5";
import { EntryPanelTemplate } from "../templates";
/**
* Function to query the card image from the Scryfall API for the specfied
* card entry. In case a matching record could be found, the url to the image will
* be returned, otherwise an empty string will be returned.
*/
const getImage = async (entry: CardEntry) => {
// preparing card name to be compatible with the API
const escapedName = entry.name.replace("&", "and");
const requestUrl = `https://api.scryfall.com/cards/search?q=name:"${escapedName}" AND set:${entry.set.id}`;
const resp: Response = await fetch(requestUrl);
const json = await resp.json();
if (json.data && json.data.length > 0) {
const obj = json.data[0];
return obj.image_uris.normal;
}
return "";
};
/**
* Panel to display the details on a selected Magic card entry.
* The panel needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * entry - `CardEntry` object that contains the data that should be displayed.
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
**/
const SelectedMtgPanel: React.FC<{
entry: CardEntry;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const extraAttributes = [
{accessKey: "foil", icon: <IoSparklesSharp />},
]
return (
<EntryPanelTemplate
entry={props.entry}
defaultImageUrl="https://gamepedia.cursecdn.com/mtgsalvation_gamepedia/f/f8/Magic_card_back.jpg"
extraAttributes={extraAttributes}
fetchEntryPreviewImage={getImage}
setImageModalImages={props.setImageModalImages}
setImageModalImageIndex={props.setImageModalImageIndex}
setImageModalVisible={props.setImageModalVisible}
/>
);
};
export default SelectedMtgPanel;
@@ -0,0 +1,10 @@
import CreateEditMtgModal, {Mode} from "./CreateEditMtgModal";
import MtgTable from "./MtgTable";
import SelectedMtgPanel from "./SelectedMtgPanel";
export {
CreateEditMtgModal,
Mode,
MtgTable,
SelectedMtgPanel
};
@@ -0,0 +1,62 @@
import React, { Dispatch, SetStateAction } from "react";
import ModalTemplate from "../templates/ModalTemplate";
/**
* A modal to ask for confirmation and trigger actions based on the decision.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
* * confirmAction - Function that should be executed when the user confirms the action.
* * abortAction - (Optional) function that should be executed when the user aborts the action. By default, the modal is just closed.
* * title - (Optional) title of the modal, default is 'Confirmation'.
* * text - (Optional) text of the modal, default is 'Do you want to proceed?'.
*/
const ConfirmationModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
confirmAction: Function;
abortAction?: Function;
title?: string;
text?: string;
}> = (props) => {
const confirmAction = () => {
props.confirmAction();
props.setVisible(false);
}
const abortAction = () => {
if(props.abortAction) {
props.abortAction();
}
props.setVisible(false);
}
return (
<>
{props.visible ? (
<ModalTemplate
title={props.title ? props.title : "Confirmation"}
onClickCloseIcon={() => props.setVisible(false)}
modalStyle="w-[60%] h-[20%] lg:w-[50%] xl:w-[35%] 2xl:w-[30%]"
>
<div className="relative justify-center items-center text-center text-gray-600 mx-8">
<div>
{props.text ? props.text : "Do you want to proceed?"}
</div>
<div className="my-8 flex text-center items-center justify-center">
<button type="submit" className="mx-2" onClick={() => confirmAction()}>Confirm</button>
<button type="submit" className="mx-2" onClick={() => abortAction()}>Abort</button>
</div>
</div>
</ModalTemplate>
) : (
""
)}
</>
);
};
export default ConfirmationModal;
@@ -0,0 +1,121 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { GrNext, GrPrevious } from "react-icons/gr";
import { RotatingLines } from "react-loader-spinner";
import ModalTemplate from "../templates/ModalTemplate";
import Image from "next/image";
/**
* Modal to display a set of images.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
* * game - Name of the game for which images should be displayed.
* * images - List of image names that should be displayed (only name, no paths).
* * startIndex - (Optional) index that indicates at which element of the list provided via `props.images` the display should start.
*/
const ImageModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
game: string;
images: string[];
startIndex?: number;
}> = (props) => {
// current image as base-64 string
const [imageB64, setImageB64] = useState<string>("");
// index of current image in `props.images`
const [imageIndex, setImageIndex] = useState<number>(0);
// flag to display the loading image
const [loaderVisible, setLoaderVisible] = useState<boolean>(true);
useEffect(() => {
if (props.visible && props.images) {
setLoaderVisible(true);
setImageIndex(props.startIndex | 0);
loadImage(props.startIndex | 0);
}
}, [props.visible]);
const loadImage = async (id: number) => {
invoke("get_image_b64", { image: props.images[id], game: props.game }).then(
(result) => {
setImageB64(result as string);
setLoaderVisible(false);
}
);
};
const loadNextImage = () => {
if(imageIndex < props.images.length -1){
setLoaderVisible(true);
setImageIndex(imageIndex + 1);
loadImage(imageIndex + 1);
}
}
const loadPreviousImage = () => {
if(imageIndex > 0){
setLoaderVisible(true);
setImageIndex(imageIndex - 1);
loadImage(imageIndex - 1);
}
}
return (
<>
{props.visible ? (
<ModalTemplate
title="Image"
onClickCloseIcon={() => props.setVisible(false)}
modalStyle=" w-[80%] h-[80%]"
>
<div className="relative h-[90%] mx-8">
{loaderVisible
?
<div className="w-full h-[90%] flex items-center justify-center">
<RotatingLines
strokeColor="grey"
strokeWidth="5"
animationDuration="0.75"
width="96"
/>
</div>
:
<Image src={imageB64} layout="fill" objectFit="contain" />
}
{/* next/previous image icons */}
{props.images.length > 1 ? (
<div className="absolute flex justify-between z-20 top-[50%] w-full">
{imageIndex > 0 ? (
<div className="ml-4 hover:scale-110 hover:rounded-full hover:shadow-xl shadow-gray-500 cursor-pointer" onClick={() => loadPreviousImage()}>
<GrPrevious className="test-gray-600" />
</div>
) : (
<div></div>
)}
{imageIndex < props.images.length - 1 ? (
<div className="mr-4 hover:scale-110 hover:rounded-full hover:shadow-xl shadow-gray-500 cursor-pointer" onClick={() => loadNextImage()}>
<GrNext className="test-gray-600" />
</div>
) : (
<div></div>
)}
</div>
) : (
""
)}
</div>
</ModalTemplate>
) : (
""
)}
</>
);
};
export default ImageModal;
@@ -0,0 +1,45 @@
import React, { Dispatch, SetStateAction } from "react";
import ModalTemplate from "../templates/ModalTemplate";
/**
* A modal to notify the user.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
* * title - (Optional) title of the modal, default is 'Notification'.
* * text - (Optional) text of the modal, default is 'Process completed successfully.'.
*/
const NotificationModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
title?: string;
text?: string;
}> = (props) => {
return (
<>
{props.visible ? (
<ModalTemplate
title={props.title ? props.title : "Notification"}
onClickCloseIcon={() => props.setVisible(false)}
modalStyle="w-[60%] h-[20%] lg:w-[50%] xl:w-[35%] 2xl:w-[30%]"
>
<div className="relative justify-center items-center text-center text-gray-600 mx-8">
<div>
{props.text ? props.text : "Process completed successfully."}
</div>
<div className="my-8 flex text-center items-center justify-center">
<button type="submit" className="mx-2" onClick={() => props.setVisible(false)}>Ok</button>
</div>
</div>
</ModalTemplate>
) : (
""
)}
</>
);
};
export default NotificationModal;
@@ -0,0 +1,118 @@
import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import { Configuration } from "../../types";
import ModalTemplate from "../templates/ModalTemplate";
/**
* Modal to overwrite the general app settings.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
*/
const SettingsModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
// Configuration object
const [config, setConfig] = useState<Configuration>(null);
// Supported Games
const [games, setGames] = useState<string[]>([]);
const gameRef = useRef<HTMLSelectElement>();
// load default game options the first time this component is loaded
useEffect(() => {
invoke("get_game_variants_json")
.then(result => setGames(JSON.parse(result as string)));
}, []);
// reload config from backend whenever this modal becomes visible
useEffect(() => {
if (props.visible) {
loadConfig();
}
}, [props.visible]);
// load configuration object from backend and store it to the state variable `config`.
const loadConfig = async () => {
const configObj = await invoke("get_configuration_json").then((config) =>
JSON.parse(config as string)
);
setConfig(configObj);
gameRef.current.value = configObj.defaultGame;
};
// send the state variable `config` to the backend in order to overwrite the general
// app config.
const saveConfig = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
invoke("store_configuration", { obj: JSON.stringify(config) }).then(() => {
props.setVisible(false);
});
};
// callback function to select the collection data store directory
const selectStorageDir = async () => {
const selected = await open({
multiple: false,
directory: true,
title: "Select Directory",
});
if (selected) {
let tmpConfig = config;
tmpConfig.dataStorage = selected as string;
setConfig({ ...tmpConfig });
}
};
const selectDefaultGame = () => {
let tmpConfig = config;
tmpConfig.defaultGame = gameRef.current.value;
setConfig({ ...tmpConfig });
}
return (
<>
{props.visible ? (
<ModalTemplate
title="Settings"
onClickCloseIcon={() => props.setVisible(false)}
modalStyle="w-[60%] h-[25%] lg:w-[50%] xl:w-[35%] 2xl:w-[30%]"
>
<div className="relative flex justify-center items-center text-gray-600 mx-8">
<form onSubmit={(e) => saveConfig(e)}>
<div className="grid grid-settings-8 gap-x-4 gap-y-4">
<label className="text-sm col-span-1">Storage Directory</label>
<input
className="col-span-7 text-sm border-2 cursor-pointer hover:bg-gray-200 hover:underline"
onClickCapture={() => selectStorageDir()}
value={config ? config.dataStorage : ""}
/>
<label className="text-sm col-span-1">Default Game</label>
<select
className="col-span-7 text-sm border-2"
ref={gameRef}
onChange={() => selectDefaultGame()}
>
{games.map(game => <option value={game}>{game}</option>)}
</select>
</div>
<div className="my-4 text-center">
<button type="submit">Save</button>
</div>
</form>
</div>
</ModalTemplate>
) : (
""
)}
</>
);
};
export default SettingsModal;
@@ -0,0 +1,11 @@
import ConfirmationModal from "./ConfirmationModal";
import ImageModal from "./ImageModal";
import NotificationModal from "./NotificationModal";
import SettingsModal from "./SettingsModal";
export {
ConfirmationModal,
ImageModal,
NotificationModal,
SettingsModal
};
@@ -0,0 +1,68 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/pokemon";
import { CreateEditModalTemplate } from "../templates";
// Enum to control wether the modal is in "Create" or "Edit" mode.
export enum Mode {
Create,
Edit,
}
/**
* Modal to create a new Pokemon card entry or edit an existing one.
* The modal needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
*
* * mode - Flag to specifiy if the modal should be in "Create new Entry" or "Edit existing Entry" node.
*
* * collection - Reference to the current Pokemon `CardEntry` collection.
* * setCollection - Function to set/update the collection list to add new entries or update existing ones.
*
* * selectedEntry - Reference to the currently selected `CardEntry` entry that should be edited in case the modal is in `Edit` mode.
* * setSelectedEntry - Function to set/update the currently selected `CardEntry` entry.
*
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
*/
const CreateEditPokemonModal: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
collection: CardEntry[];
mode: Mode;
selectedEntry: CardEntry;
setCollection: Dispatch<SetStateAction<CardEntry[]>>;
setSelectedEntry: Dispatch<SetStateAction<CardEntry>>;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const extraAttributes = [
{ label: "Holo", accessKey: "holo" },
{ label: "1. Edition", accessKey: "firstEdition" },
];
return (
<CreateEditModalTemplate
visible={props.visible}
setVisible={props.setVisible}
game="Pokemon"
extraAttributes={extraAttributes}
selectedEntry={props.selectedEntry}
setSelectedEntry={props.setSelectedEntry}
mode={props.mode}
collection={props.collection}
setCollection={props.setCollection}
setImageModalImages={props.setImageModalImages}
setImageModalImageIndex={props.setImageModalImageIndex}
setImageModalVisible={props.setImageModalVisible}
/>
);
};
export default CreateEditPokemonModal;
@@ -0,0 +1,16 @@
import React from "react";
/**
* Rebuilding of the 1. Edition Icon
*/
const IconPokemonFirstEdition: React.FC<{className?: string}> = (props) => {
return (
<div className={`flex items-center justify-center ${props.className ? props.className: ""}`}>
<div className="rounded-full bg-black text-white px-1 text-xs font-bold">
1
</div>
</div>
)
}
export default IconPokemonFirstEdition;
@@ -0,0 +1,49 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/pokemon";
import { TableTemplate } from "../templates";
import { IoSparklesSharp } from "react-icons/io5";
import { BsPencilFill, BsPaletteFill } from "react-icons/bs";
import IconPokemonFirstEdition from "./IconPokemonFirstEdition";
/**
* Table to display a collection of Pokemon cards and select entries from it.
*
* # Props:
* * collection - List of `CardEntry` objects that should be displayed via the table.
* * selectedEntry - `CardEntry` object that is currently selected by the user.
* * setCollection - Function to set/update the collection list that should be displayed.
* * setSelectedEntry - Function to specifiy, which entry from the table the user has currenty selected.
*/
const PokemonTable: React.FC<{
collection: CardEntry[];
selectedEntry: CardEntry;
setCollection: Dispatch<SetStateAction<CardEntry[]>>;
setSelectedEntry: Dispatch<SetStateAction<CardEntry>>;
}> = (props) => {
const tableFields = [
{label: "Name", valueKey: "name", sortKey: "name"},
{label: "Set", valueKey: "set.name", sortKey: "set.releaseDate"},
{label: "Language", valueKey: "language", sortKey: "language"},
{label: "Condition", valueKey: "condition", sortKey: "condition"},
{label: "#", valueKey: "amount", sortKey: "amount"},
{label: "Holo", valueKey: "holo", sortKey: "holo", icon: <IoSparklesSharp /> },
{label: "FirstEdition", valueKey: "firstEdition", sortKey: "firstEdition", icon: <IconPokemonFirstEdition /> },
{label: "Signed", valueKey: "signed", sortKey: "signed", icon: <BsPencilFill /> },
{label: "Altered", valueKey: "altered", sortKey: "altered", icon: <BsPaletteFill /> },
{label: "Note", valueKey: "note", sortKey: "note"}
];
return (
<TableTemplate
tableFields={tableFields}
collection={props.collection}
selectedEntry={props.selectedEntry}
setCollection={props.setCollection}
setSelectedEntry={props.setSelectedEntry}
/>
);
};
export default PokemonTable;
@@ -0,0 +1,73 @@
import React, { Dispatch, SetStateAction } from "react";
import { CardEntry } from "../../types/pokemon";
import IconPokemonFirstEdition from "./IconPokemonFirstEdition";
import { IoSparklesSharp } from "react-icons/io5";
import { EntryPanelTemplate } from "../templates";
/**
* Function to query the card image from the Pokemon TCG API for the specfied
* card entry. In case a matching record could be found, the url to the image will
* be returned, otherwise an empty string will be returned.
*/
const getImage = async (entry: CardEntry) => {
// preparing card name to be compatible with the API
const escapedName = entry.name
.replace("&", "*")
.replace(" EX", "-EX")
.replace(" GX", "-GX");
// if the set number of the entry is maintainend, we will query via set-id + set-no.
// This is especially helpfull when there are multiple artworks of the same card in the
// same set.
const reqViaId = `https://api.pokemontcg.io/v2/cards?q=id:"${entry.set.id}-${entry.setNo}"`;
// if set number is not maintainend, we will query via card name + set-id.
const reqViaNameAndSet = `https://api.pokemontcg.io/v2/cards?q=name:"${escapedName}" AND set.id:${entry.set.id}`;
const resp: Response = await fetch(entry.setNo && entry.setNo != "" ? reqViaId : reqViaNameAndSet);
const json = await resp.json();
if (json.data && json.data.length > 0) {
const obj = json.data[0];
return obj.images.small;
}
return "";
};
/**
* Panel to display the details of a selected Pokemon card entry.
* The panel needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * entry - `CardEntry` object that contains the data that should be displayed.
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
**/
const SelectedPokemonPanel: React.FC<{
entry: CardEntry;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const extraAttributes = [
{accessKey: "holo", icon: <IoSparklesSharp />},
{accessKey: "firstEdition", icon: <IconPokemonFirstEdition />},
]
return (
<EntryPanelTemplate
entry={props.entry}
defaultImageUrl="https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg"
extraAttributes={extraAttributes}
fetchEntryPreviewImage={getImage}
setImageModalImages={props.setImageModalImages}
setImageModalImageIndex={props.setImageModalImageIndex}
setImageModalVisible={props.setImageModalVisible}
/>
);
};
export default SelectedPokemonPanel;
@@ -0,0 +1,10 @@
import CreateEditPokemonModal, {Mode} from "./CreateEditPokemonModal";
import PokemonTable from "./PokemonTable";
import SelectedPokemonPanel from "./SelectedPokemonPanel";
export {
CreateEditPokemonModal,
Mode,
PokemonTable,
SelectedPokemonPanel
};
@@ -0,0 +1,424 @@
import React, {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import ModalTemplate from "../templates/ModalTemplate";
import { SetTemplate, EntryTemplate } from "../../types";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import { VscClose } from "react-icons/vsc";
import IntegerInput from "../templates/IntegerInput";
import ConfirmationModal from "../modals/ConfirmationModal";
// Enum to control wether the modal is in "Create" or "Edit" mode.
export enum Mode {
Create,
Edit,
}
/**
* Configuration object to specify additional, game-specific binary attributes of an entry.
* The specified label will be displayed for a corresponding checkbox that represents
* this attribute. The accessKey is used to retrieve the current value of the attribute
* for an entry that is editied as well as to send a new/updated value of the attribute
* to the backend.
*/
type ExtraAttribute = {
label: string;
accessKey: string;
}
/**
* Template modal to create a new collection entries or edit existing ones.
* The modal needs to be connected via a image display modal via three functions
* that control this image modal.
*
* # Props:
* * visible - Flag to indicate wheter the modal should be displayed or not.
* * setVisible - Function to change the value of prop `visible`.
*
* * game - Identifier of the game for which this modal should create or edit entries.
* This is required to identify the correct collection and corresponding file system
* directories.
*
* * extraAttributes - List of additional, games-specific binary attributes beyond the standard binary
* attributes `signed` and `altered`
*
* * mode - Flag to specifiy if the modal should be in "Create new Entry" or "Edit existing Entry" node.
*
* * collection - Reference to the current entry collection.
* * setCollection - Function to set/update the collection list to add new entries or update existing ones.
*
* * selectedEntry - Reference to the currently selected entry that should be edited in case the modal is in `Edit` mode.
* * setSelectedEntry - Function to set/update the currently selected entry.
*
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
*/
const CreateEditModalTemplate: React.FC<{
visible: boolean;
setVisible: Dispatch<SetStateAction<boolean>>;
game: string;
extraAttributes: ExtraAttribute[]
collection: EntryTemplate[];
mode: Mode;
selectedEntry: EntryTemplate;
setCollection: Dispatch<SetStateAction<EntryTemplate[]>>;
setSelectedEntry: Dispatch<SetStateAction<EntryTemplate>>;
setImageModalImages: Dispatch<SetStateAction<string[]>>;
setImageModalImageIndex: Dispatch<SetStateAction<number>>;
setImageModalVisible: Dispatch<SetStateAction<boolean>>;
}> = (props) => {
const [sets, setSets] = useState<SetTemplate[]>([]);
const [languages, setLanguages] = useState<string[]>([]);
const [conditions, setConditions] = useState<string[]>([]);
// confirmation modal visibilities
const [abortConfirmationModalVisibility, setAbortConfirmationModalVisiblity] = useState<boolean>(false);
// form input element refs
const nameRef = useRef<HTMLInputElement>(null);
const setRef = useRef<HTMLSelectElement>(null);
const setNoRef = useRef<HTMLInputElement>(null);
const languageRef = useRef<HTMLSelectElement>(null);
const conditionRef = useRef<HTMLSelectElement>(null);
const amountRef = useRef<HTMLInputElement>(null);
const signedRef = useRef<HTMLInputElement>(null);
const alteredRef = useRef<HTMLInputElement>(null);
const noteRef = useRef<HTMLInputElement>(null);
// dynamic creation of refs for extra attributes
let extraAttributesRefs = {};
props.extraAttributes.map(attribute => extraAttributesRefs[attribute.accessKey] = useRef<HTMLInputElement>(null));
// images as state variable for better handling
const [images, setImages] = useState<string[]>([]);
// The first time this modal gets rendered, it fetches language and condition
// informations from the backened.
useEffect(() => {
invoke("get_language_variants_json").then((result) => {
const obj = JSON.parse(result as string) as string[];
setLanguages(obj);
});
invoke("get_condition_variants_json").then((result) => {
const obj = JSON.parse(result as string) as string[];
setConditions(obj);
});
}, []);
// everytime the modal becomes visible in "Edit" mode, it populates
// its fields with the data from the selected entry.
useEffect(() => {
if (props.visible) {
// get set data. We need to do this each time the element
// becomes visible, because the user might have updated the set
// data in between.
invoke("get_sets", {game: props.game}).then((result) => {
const obj = JSON.parse(result as string) as SetTemplate[];
setSets(obj);
});
// populate fields with data in case of 'edit' mode
if (props.mode == Mode.Edit) {
nameRef.current!.value = props.selectedEntry.name;
setRef.current!.value = props.selectedEntry.set.id.toLowerCase();
setNoRef.current!.value = props.selectedEntry.setNo;
languageRef.current!.value = props.selectedEntry.language;
conditionRef.current!.value = props.selectedEntry.condition;
amountRef.current!.value = `${props.selectedEntry.amount}`;
noteRef.current!.value = props.selectedEntry.note;
signedRef.current!.checked = props.selectedEntry.signed;
alteredRef.current!.checked = props.selectedEntry.altered;
// populate dynamic extra attribute fields
props.extraAttributes.map(attribute => extraAttributesRefs[attribute.accessKey].current!.checked = props.selectedEntry[attribute.accessKey]);
setImages(props.selectedEntry.images);
}
} else {
// clear image state in any case the modal gets closed
setImages([]);
}
}, [props.visible]);
// handler when user aborts maintaining an entry by clicking the close icon
const onClose = () => {
// if "Create" mode, delete all temporary stored images
if (props.mode == Mode.Create) {
images.forEach((image) => {
invoke("delete_image", { image: image, game: props.game });
});
}
// if "Edit" mode, delete all newly added images
if (props.mode == Mode.Edit) {
images
.filter((image) => !props.selectedEntry.images.includes(image))
.forEach((image) => {
invoke("delete_image", { image: image, game: props.game });
});
}
props.setVisible(false);
};
// get temporary entry from the current values stored in all input fields
const getTempEntry = () => {
let cardEntry: EntryTemplate = {
id: props.mode == Mode.Create ? 0 : props.selectedEntry.id,
name: nameRef.current!.value,
set: sets.filter((set) => set.id === setRef.current!.value)[0],
setNo: setNoRef.current!.value,
language: languageRef.current!.value,
condition: conditionRef.current!.value,
amount: Number.parseInt(amountRef.current!.value),
altered: alteredRef.current!.checked,
signed: signedRef.current!.checked,
note: noteRef.current!.value,
images: images,
};
// get values of extra attributes
props.extraAttributes.map(attribute => cardEntry[attribute.accessKey] = extraAttributesRefs[attribute.accessKey].current!.checked);
return cardEntry;
};
// submit an entry to the backend based on the current values of all input fields.
// if "Edit" mode, the existing entry will be overwritten.
const submitEntry = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
let cardEntry = getTempEntry();
if (props.mode == Mode.Create) {
invoke("add_card", { obj: JSON.stringify(cardEntry), game: props.game })
.then((result) => {
cardEntry.id = Number.parseInt(JSON.parse(result as string));
props.setCollection(props.collection.concat(cardEntry));
props.setVisible(false);
})
.catch((reject) => console.log(reject));
}
if (props.mode == Mode.Edit) {
cardEntry.id = props.selectedEntry.id;
invoke("update_card", { obj: JSON.stringify(cardEntry), game: props.game })
.then(() => {
props.setCollection(
props.collection.map((entry) =>
entry.id == cardEntry.id ? cardEntry : entry
)
);
props.setSelectedEntry(cardEntry);
props.setVisible(false);
})
.catch((reject) => console.log(reject));
}
};
// trigger the image selection dialog, copy the selected image via the backend service and
// store the returned image name in the temporary list of image names.
const addImage = async () => {
// only add image if name field has a value
if (nameRef.current && nameRef.current.reportValidity()) {
const selected = await open({
multiple: true,
title: "Select image",
filters: [
{
name: "Image",
extensions: ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG"],
},
],
});
if (selected && Array.isArray(selected)) {
let cardEntry = getTempEntry();
for(let i = 0; i < selected.length; i++) {
const imgId = await invoke("copy_image", {
obj: JSON.stringify(cardEntry),
imgLocation: selected[i],
newEntry: props.mode == Mode.Create,
game: props.game
});
cardEntry.images.push(imgId as string);
}
setImages([].concat(cardEntry.images)); // using the concat with an empty array is a trick to trigger the rerender, otherwise the state does recognize to rerender
}
}
};
// Enable the connected image modal and start the display by the provided image index.
const displayImage = async (index: number) => {
props.setImageModalImages(images);
props.setImageModalImageIndex(index);
props.setImageModalVisible(true);
};
// Delete the image with the specified image name by calling the corresponding backend service
// and remove the image name from the temporary list.
const deleteImage = async (image: string) => {
invoke("delete_image", { image: image, game: props.game })
.then(() => {
setImages(images.filter((img) => img != image));
})
.catch((reject) => console.log(reject));
};
return (
<>
{props.visible ? (
<ModalTemplate
title={props.mode == Mode.Create ? "Create Entry" : "Edit Entry"}
onClickCloseIcon={() => setAbortConfirmationModalVisiblity(true)}
modalStyle="w-[60%] h-[70%] lg:w-[50%] xl:w-[35%] 2xl:w-[30%]"
>
<div className="relative flex justify-center items-center text-gray-600 mx-8">
<form onSubmit={(e) => submitEntry(e)}>
<div className="grid grid-settings-8 gap-x-4 gap-y-4">
<label className="text-sm col-span-1">Name</label>
<input
className="text-sm col-span-7 border-2"
type="text"
required={true}
autoFocus={true}
ref={nameRef}
/>
<label className="text-sm col-span-1">Set</label>
<select className="text-sm col-span-7 border-2" ref={setRef}>
{sets.map((set) => (
<option value={`${set.id}`}>{set.name}</option>
))}
</select>
<label className="text-sm col-span-1">Set No.</label>
<input type="number"
className="text-sm col-span-2 border-2"
defaultValue={""}
ref={setNoRef}
/>
<div className="col-span-5" />
<label className="text-sm col-span-1">Language</label>
<select
className="text-sm col-span-3 border-2"
ref={languageRef}
>
{languages.map((language) => (
<option value={language}>{language}</option>
))}
</select>
<div className="col-span-4" />
<label className="text-sm col-span-1">Condition</label>
<select
className="text-sm col-span-3 border-2"
ref={conditionRef}
>
{conditions.map((condition) => (
<option value={condition}>{condition}</option>
))}
</select>
<div className="col-span-4" />
<label className="text-sm col-span-1">Amount</label>
<IntegerInput
className="text-sm col-span-2 border-2"
required={true}
ref={amountRef}
/>
<div className="col-span-5" />
<div className="text-sm col-span-1" />
<div className="text-sm col-span-7 flex justify-between">
{/* fields for dynamic extra attributes */}
{props.extraAttributes.map(attribute =>
<div>
<input type="checkbox" ref={extraAttributesRefs[attribute.accessKey]} />
<label className="mx-2">{attribute.label}</label>
</div>
)}
{/* default attributes */}
<div>
<input type="checkbox" ref={signedRef} />
<label className="mx-2">Signed</label>
</div>
<div>
<input type="checkbox" ref={alteredRef} />
<label className="mx-2">Altered</label>
</div>
</div>
<label className="text-sm col-span-1">Note</label>
<input
className="text-sm col-span-7 border-2"
type="text"
ref={noteRef}
/>
<label className="text-sm col-span-1">Images</label>
<div className="text-sm col-span-7">
{images.map((value, index) => (
<div className="flex items-center">
<div
className="hover:bg-blue-50 cursor-pointer"
id={index.toString()}
onClick={() => displayImage(index)}
>
Image {index + 1}
</div>
<div
className="mx-4 cursor-pointer hover:scale-125"
onClick={() => deleteImage(value)}
>
<VscClose className="text-red-600 text-lg" />
</div>
</div>
))}
<button
type="button"
className="my-4"
onClick={() => addImage()}
>
Add
</button>
</div>
</div>
<div className="my-4 text-center">
<button type="submit">Submit</button>
</div>
</form>
</div>
</ModalTemplate>
) : (
""
)}
<ConfirmationModal
visible={abortConfirmationModalVisibility}
setVisible={setAbortConfirmationModalVisiblity}
title={
props.mode == Mode.Create
? "Abort Creating"
: "Abort Editing"
}
text={
props.mode == Mode.Create
? "Do you reall want to abort the current entry creation?"
: "Do you really want to abort the current entry editing?"
}
confirmAction={() => onClose()}
/>
</>
);
};
export default CreateEditModalTemplate;
@@ -0,0 +1,125 @@
import React, { useEffect, useState } from "react";
import { BsPencilFill, BsPaletteFill } from "react-icons/bs";
import { EntryTemplate } from "../../types";
/**
* Configuration object to specify additional, game-specific binary attributes of an entry.
* The specified icon will be displayed alongside the default binary attributes
* `signed` and `altered`, if the value of the entry, that is retrieved by using
* the access key of this configuration object is `true`.
*/
type ExtraAttribute = {
accessKey: string;
icon?: React.FC|JSX.Element
}
/**
* Template panel to display the details on of an entry.
* The panel needs to be connected with a image display modal via three functions
* that control this image modal.
*
* # Props:
* * entry - entry object that contains the data that should be displayed.
* * defaultImageUrl - Url to the default image that should be displayed as a preview image of the entry
* * extraAttributes - List of additional binary attributes beyond the standard binary attributes `signed` and `altered`
* * fetchEntryPreviewImage - Function to fetch the specific preview image for the entry object specified by property `entry`.
* * setImageModalImages - Function to pass a list of image names to the connected image modal.
* * setImageModalImageIndex - Function to set the starting image index of the connected image modal.
* * setImageModalVisible - Function to control the visiblity of the connected image modal.
**/
const EntryPanelTemplate: React.FC<{
entry: EntryTemplate;
defaultImageUrl: string;
extraAttributes: ExtraAttribute[];
fetchEntryPreviewImage: Function;
setImageModalImages: Function;
setImageModalImageIndex: Function;
setImageModalVisible: Function;
}> = (props) => {
// preview image of the card entry, default image or value that is fetched via `props.fetchEntryPreviewImage`.
const [previewImage, setPreviewImage] = useState<string>("");
// if entry reference changes, the preview image of the corresponding
// card gets fetched. If no image could be found, the default image will be displayed.
useEffect(() => {
if (props.entry) {
props.fetchEntryPreviewImage(props.entry)
.then((result: string) => setPreviewImage(result));
}
else {
// in this case the current selected entry was deleted. Therefore, we also reset the preview image
setPreviewImage("");
}
}, [props.entry]);
// trigger the connected image modal to display the image with the specfied index from
// the list of images of the currently selected entry.
const displayImage = async (index: number) => {
props.setImageModalImages(props.entry.images);
props.setImageModalImageIndex(index);
props.setImageModalVisible(true);
};
return (
<>
<div className="fixed mt-4 z-[1]">
<div>
{previewImage && previewImage != ""
? <img src={previewImage} width={250} />
: <img src={props.defaultImageUrl} width={250} />
}
</div>
{props.entry ? (
<div className="grid grid-settings-2 gap-x-4 gap-y-2 mt-4">
<p>Name</p>
<p>{props.entry.name}</p>
<p>Set</p>
<p>{props.entry.set.name}</p>
{props.entry.setNo && props.entry.setNo != ""
? <>
<p>Set No.</p>
<p>{props.entry.set.id.toUpperCase()}-{props.entry.setNo}</p>
</>
: ""
}
<p>Language</p>
<p>{props.entry.language}</p>
<p>Condition</p>
<p>{props.entry.condition}</p>
<p>Amount</p>
<p>{props.entry.amount}</p>
<p></p>
<div className="flex">
{props.extraAttributes.map(attribute =>
props.entry[attribute.accessKey] ? <>{attribute.icon}<div className="mr-2"></div></> : ""
)}
{props.entry.signed ? <BsPencilFill className="mr-2" /> : ""}
{props.entry.altered ? <BsPaletteFill className="mr-2" /> : ""}
</div>
<p className="mt-4">Note</p>
<p className="mt-4">{props.entry.note}</p>
<p>Images</p>
<div>
{props.entry.images.map((value, index) => (
<div
className="hover:bg-blue-50 cursor-pointer"
id={index.toString()}
onClick={() => displayImage(index)}
>
Image {index+1}
</div>
))}
</div>
</div>
) : (
""
)}
</div>
</>
);
};
export default EntryPanelTemplate;
@@ -0,0 +1,31 @@
import React, { forwardRef } from "react";
/**
* HTMLInput Element for numeric values with additional validation so that only positive integer
* values can be entered.
*/
const IntegerInput = forwardRef((props: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, ref: React.MutableRefObject<HTMLInputElement>) => {
const handleChange = (event: React.FormEvent<HTMLInputElement>) => {
const value = event.currentTarget.value;
if(value.includes('.') || value.includes('-') || value === ""){
ref.current.value = "1";
}
}
return (
<input
type="number"
min={props.min ? props.min : 1}
step={props.step ? props.step : 1}
defaultValue={props.defaultValue ? props.defaultValue : 1}
className={props.className ? props.className : ""}
required={props.required ? props.required : false}
ref={ref ? ref : null}
onInput={(e) => handleChange(e)}
/>
)
});
export default IntegerInput;
@@ -0,0 +1,48 @@
import React from "react";
import { AiOutlineClose } from "react-icons/ai";
/**
* General template to implement modals. This template grays out the background and displays a
* centered container in which the modal specifics can be injected via prop `children`.
*
* # Props:
* * title - Title of the modal, which will be displayed as the header of the modal container.
* * children - (Optional) React node containing the specific modal TSX.
* * modalStyle - (Optional) prop to extend/overwrite the styling of the modal container. If this prop is used,
* it needs define at least the width and height of the modal container.
* * onClickCloseIcon - (Optional) callback function that should be invoked when the user clicks on the 'close'
* icon on the top right corner of the modal container.
* * onClickBackground - (Optional) callback function that should be invoked when the user clicks on the grayed
* out background.
*/
const ModalTemplate: React.FC<{
title: string;
children?: React.ReactNode;
modalStyle?: string;
onClickCloseIcon?: Function;
onClickBackground?: Function;
}> = (props) => {
return (
<div className="absolute z-10 w-full h-full bg-black/70 flex justify-center items-center">
<div className={`rounded-lg bg-white shadow-xl ${props.modalStyle ? props.modalStyle : "w-[50%] h-[50%]" }`}>
<div className="flex justify-between pt-2 px-4 mb-8">
<div></div>
<div className="text-gray-600">{props.title}</div>
<div
className="text-gray-600 hover:scale-110 cursor-pointer"
onClick={
props.onClickCloseIcon
? () => props.onClickCloseIcon!()
: () => ""
}
>
<AiOutlineClose size={20} />
</div>
</div>
{props.children ? props.children : ""}
</div>
</div>
);
};
export default ModalTemplate;
@@ -0,0 +1,197 @@
import React, { useState } from "react";
import { EntryTemplate } from "../../types";
/**
* Table field configuration object. Each field maps to one column of the table.
*
* * label - used as column label if no icon is specfied
* * icon - (Optional) used as column label and cell value for boolean values
* * valueKey - Accessor key to the field on an data entry object that maps to the
* field described by this config object. This value will be used for display
* inside the table cell.
* For nested access, use a `.` to define nested routes (see function `getValByKey`
* for more information).
* * sortKey - Accessor key to the field on an data entry object that maps to the
* field described by this config object. This value will be used for sorting
* entries by the attrbiute that this field represents.
* For nested access, use a `.` to define nested routes (see function `getValByKey`
* for more information).
*/
type TableField = {
label: string;
icon?: React.FC | JSX.Element;
valueKey: string;
sortKey: string;
};
/**
* Helper function to support access to fields of nested objects
* with dynamic key routes as strings.
*
* e.g
* obj: {a: "hello"} keyRoute: "a" => "hello"
* obj: {a: "hello", b: {c: 42}} keyRoute: "b" => {c: 42}
* obj: {a: "hello", b: {c: 42}} keyRoute: "b.c" => 42
*/
const getValByKey = (obj: any, keyRoute: string): any => {
const keys = keyRoute.split(".");
if (keys.length == 1) {
return obj[keyRoute];
} else {
return getValByKey(obj[keys[0]], keyRoute.replace(`${keys[0]}.`, ""));
}
};
/**
* General template to display an array of arbitrary objects as a table that
* is filterable and sortable. Optionally, the table can also be configured
* to support to select an entry from it (props: selectedEntry and setSelectedEntry)
*
* The main table configuration is done via the array of `TableField` objects,
* that are specified via the property `tableFields`. Each element of `tableFields`
* is mapped to one column of the table.
*
* # Props:
* * tableFields - List of `TableField` objects to configure the columns of the table.
* * collection - List of objects that should be displayed via the table.
* * selectedEntry - (Optional) reference to the `Entry` object that is currently selected by the user.
* * setCollection - Function to set/update the collection list that should be displayed.
* * setSelectedEntry - (Optional) function to specifiy, which entry from the table the user has currenty selected.
*/
const TableTemplate: React.FC<{
tableFields: TableField[];
collection: EntryTemplate[];
selectedEntry?: EntryTemplate;
setCollection: Function;
setSelectedEntry?: Function;
}> = (props) => {
// string that is applied to each entry to filter the entries of the displayed collection
const [filter, setFilter] = useState<string>("");
// object that will receive fields overtime to indicate the sort order of columns so that
// the component knows to sort in the opposite direction every other sort request of a column
const [sortOrderByField, setSortOrderByField] = useState<Object>({});
// Callback function to sort a set of `Entry` objects by the specified field of the
// objects.
const byField = (field: string, asc: boolean) => {
return (a: EntryTemplate, b: EntryTemplate) => {
// number, bool
let x = getValByKey(a, field);
let y = getValByKey(b, field);
// string
if (typeof x === "string") {
x = x.toLowerCase();
y = y.toLowerCase();
}
// ascending
if (asc) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
} else {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
}
// descending
return 0;
};
};
const sortByField = (field: string) => {
let order = Object.hasOwn(sortOrderByField, field)
? sortOrderByField[field]
: true;
props.setCollection([...props.collection].sort(byField(field, order)));
sortOrderByField[field] = !order;
setSortOrderByField(sortOrderByField);
};
const applyFilter = (entry: EntryTemplate) => {
let result = false;
for (let i = 0; i < props.tableFields.length; i++) {
const val = getValByKey(entry, props.tableFields[i].valueKey);
if (typeof val === "number" || typeof val === "string") {
if (val.toString().toLowerCase().includes(filter)) {
result = true;
break;
}
}
}
return result;
};
return (
<div className="h-full w-full">
<div>
<input
className="border-2 mb-2 border-gray-300 px-2 rounded-sm focus:border-none"
onChange={(e) => setFilter(e.currentTarget.value)}
placeholder="Filter"
/>
</div>
<div className="h-[85%] w-fit overflow-scroll">
<table className="text-sm text-left border-2 border-slate-100 ">
<thead className="sticky top-[0] bg-slate-200">
<tr id="head">
{props.tableFields.map((field) => (
<th
id={`head-${field.label}`}
className="px-2 hover:border-b-2 border-black cursor-pointer"
onClick={() => {
sortByField(field.sortKey);
}}
>
{field.icon ? <>{field.icon}</> : field.label}
</th>
))}
</tr>
</thead>
<tbody>
{props.collection
.filter((entry) => applyFilter(entry))
.map((entry) => (
<tr
className={`cursor-pointer ${
props.selectedEntry && props.selectedEntry.id == entry.id
? "bg-blue-100"
: "hover:bg-blue-50"
}`}
id={entry.id.toString()}
onClick={() => props.setSelectedEntry ? props.setSelectedEntry(entry) : ""}
>
{props.tableFields.map((field) => (
<td className="px-2">
{field.icon ? (
getValByKey(entry, field.valueKey) ? (
<>{field.icon}</>
) : (
""
)
) : (
getValByKey(entry, field.valueKey)
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="py-4"></div>
</div>
);
};
export default TableTemplate;
@@ -0,0 +1,11 @@
import ModalTemplate from "./ModalTemplate";
import CreateEditModalTemplate from "./CreateEditModalTemplate";
import EntryPanelTemplate from "./EntryPanelTemplate";
import TableTemplate from "./TableTemplate";
export {
ModalTemplate,
CreateEditModalTemplate,
EntryPanelTemplate,
TableTemplate,
};
@@ -0,0 +1,9 @@
import type { AppProps } from "next/app";
//import "../styles/globals.css";
import "../style.css";
// This default export is required in a new `pages/_app.js` file.
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
@@ -0,0 +1,246 @@
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { VscAdd, VscEdit, VscTrash } from "react-icons/vsc";
import { SettingsModal, ConfirmationModal, NotificationModal, ImageModal } from "../components/modals";
import { CreateEditPokemonModal, Mode, PokemonTable, SelectedPokemonPanel } from "../components/pokemon";
import { CreateEditMtgModal, MtgTable, SelectedMtgPanel } from "../components/magic";
import { CardEntry as PokemonCardEntry } from "../types/pokemon";
import { CardEntry as MagicCardEntry } from "../types/magic";
function App() {
const [collection, setCollection] = useState<PokemonCardEntry[] | MagicCardEntry[]>([]);
const [selectedEntry, setSelectedEntry] = useState<PokemonCardEntry | MagicCardEntry>(null);
const [activeGame, setActiveGame] = useState<string>(null);
const [createEditMode, setCreateEditMode] = useState<Mode>(Mode.Create);
const [settingsModalVisible, setSettingsModalVisible] = useState<boolean>(false);
const [createEditModalVisible, setCreateEditModalVisible] = useState<boolean>(false);
const [deleteConfirmModalVisible, setDeleteConfirmModalVisible] = useState<boolean>(false);
const [notificationModalVisible, setNotificationModalVisible] = useState<boolean>(false);
const [imageModalVisible, setImageModalVisible] = useState<boolean>(false);
const [imageModalImageIndex, setImageModalImageIndex] = useState<number>(0);
const [imageModalImages, setImageModalImages] = useState<string[]>([]);
/**
* on initial render:
* - get configuration from backend and set the active game to the default game from the config
* - connect all menu bar events with their individual actions
*/
useEffect(() => {
invoke("get_configuration_json").then((result) => {
const config = JSON.parse(result as string);
setActiveGame(config.defaultGame);
});
// listen for general menu events
listen("tauri://menu", (event) => {
if (event.payload == "settings") {
setSettingsModalVisible(true);
}
if (event.payload == "switch_game/pokemon") {
setActiveGame("Pokemon");
setSelectedEntry(null);
}
if (event.payload == "switch_game/magic") {
setActiveGame("Magic");
setSelectedEntry(null);
}
if (event.payload == "update/sets/pokemon") {
invoke("update_sets", {game: "Pokemon"})
.then(() => setNotificationModalVisible(true));
}
if (event.payload == "update/sets/magic") {
invoke("update_sets", {game: "Magic"})
.then(() => setNotificationModalVisible(true));
}
});
}, []);
/**
* Everytime the active game changes, fetch the corresponding collection
* of the active game from the backend.
*/
useEffect(() => {
if (activeGame) {
invoke("get_collection", { game: activeGame }).then((result) => {
const obj = JSON.parse(result as string);
if (activeGame == "Pokemon")
setCollection(Object.values(obj) as PokemonCardEntry[]);
if (activeGame == "Magic")
setCollection(Object.values(obj) as MagicCardEntry[]);
});
}
}, [activeGame]);
const deleteSelectedCard = () => {
invoke("delete_card", { id: selectedEntry.id, game: activeGame }).then(
(result) => {
if (activeGame == "Pokemon")
setCollection(
(collection as PokemonCardEntry[]).filter(
(entry) => entry.id != selectedEntry.id
)
);
if (activeGame == "Magic")
setCollection(
(collection as MagicCardEntry[]).filter(
(entry) => entry.id != selectedEntry.id
)
);
setSelectedEntry(null);
}
);
};
return (
<div>
<div className="fixed flex w-full h-full z-[1] m-4">
<div className="w-[30%] 2xl:w-[20%]">
<div className="flex">
<div
className="mr-2 border-2 rounded-sm border-gray-600 p-2 shadow-lg shadow-gray-400 cursor-pointer hover:scale-105"
onClick={() => {
setCreateEditMode(Mode.Create);
setCreateEditModalVisible(true);
}}
>
<VscAdd />
</div>
<div
className={`mr-2 border-2 rounded-sm border-gray-600 p-2 shadow-lg shadow-gray-400 cursor-pointer hover:scale-105 ${
selectedEntry ? "" : "invisible"
}`}
onClick={() => {
setCreateEditMode(Mode.Edit);
setCreateEditModalVisible(true);
}}
>
<VscEdit />
</div>
<div
className={`mr-2 border-2 rounded-sm border-gray-600 p-2 shadow-lg shadow-gray-400 cursor-pointer hover:scale-105 ${
selectedEntry ? "" : "invisible"
}`}
onClick={() => setDeleteConfirmModalVisible(true)}
>
<VscTrash />
</div>
</div>
{activeGame == "Pokemon" ? (
<SelectedPokemonPanel
entry={selectedEntry as PokemonCardEntry}
setImageModalImages={setImageModalImages}
setImageModalImageIndex={setImageModalImageIndex}
setImageModalVisible={setImageModalVisible}
/>
) : (
""
)}
{activeGame == "Magic" ? (
<SelectedMtgPanel
entry={selectedEntry as MagicCardEntry}
setImageModalImages={setImageModalImages}
setImageModalImageIndex={setImageModalImageIndex}
setImageModalVisible={setImageModalVisible}
/>
) : (
""
)}
</div>
<div className="w-[70%] h-[95%] 2xl:w-[80%]">
{activeGame == "Pokemon" ? (
<PokemonTable
collection={collection as PokemonCardEntry[]}
selectedEntry={selectedEntry as PokemonCardEntry}
setCollection={setCollection}
setSelectedEntry={setSelectedEntry}
/>
) : (
""
)}
{activeGame == "Magic" ? (
<MtgTable
collection={collection as MagicCardEntry[]}
selectedEntry={selectedEntry as MagicCardEntry}
setCollection={setCollection}
setSelectedEntry={setSelectedEntry}
/>
) : (
""
)}
</div>
</div>
<div></div>
<SettingsModal
visible={settingsModalVisible}
setVisible={setSettingsModalVisible}
/>
{activeGame == "Pokemon" ? (
<CreateEditPokemonModal
visible={createEditModalVisible}
setVisible={setCreateEditModalVisible}
selectedEntry={selectedEntry as PokemonCardEntry}
setSelectedEntry={setSelectedEntry}
mode={createEditMode}
collection={collection as PokemonCardEntry[]}
setCollection={setCollection}
setImageModalImages={setImageModalImages}
setImageModalImageIndex={setImageModalImageIndex}
setImageModalVisible={setImageModalVisible}
/>
) : (
""
)}
{activeGame == "Magic" ? (
<CreateEditMtgModal
visible={createEditModalVisible}
setVisible={setCreateEditModalVisible}
selectedEntry={selectedEntry as MagicCardEntry}
setSelectedEntry={setSelectedEntry}
mode={createEditMode}
collection={collection as MagicCardEntry[]}
setCollection={setCollection}
setImageModalImages={setImageModalImages}
setImageModalImageIndex={setImageModalImageIndex}
setImageModalVisible={setImageModalVisible}
/>
) : (
""
)}
<ImageModal
visible={imageModalVisible}
setVisible={setImageModalVisible}
game={activeGame}
images={imageModalImages}
startIndex={imageModalImageIndex}
/>
<ConfirmationModal
visible={deleteConfirmModalVisible}
setVisible={setDeleteConfirmModalVisible}
confirmAction={deleteSelectedCard}
title="Delete Entry"
text="Do you really want to delete this entry?"
/>
<NotificationModal
visible={notificationModalVisible}
setVisible={setNotificationModalVisible}
title="Sets Update"
text="Sets were updated successfully."
/>
</div>
);
}
export default App;
+22
View File
@@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/*todo: document this class */
.grid-settings-8 {
grid-template-columns: max-content repeat(7, minmax(0, 1fr));
}
.grid-settings-2 {
grid-template-columns: max-content max-content
}
@layer base {
button, button[type=submit], button[type=button] {
@apply bg-gradient-to-r from-blue-900 to-blue-700 text-white uppercase px-4 rounded-sm hover:shadow-lg hover:scale-105 bg-[#ffffff]
}
}
@@ -0,0 +1,32 @@
export type Configuration = {
dataStorage: string;
defaultGame: string;
}
/**
* Simplest type of a set. All set types need to match at least the
* required fields of this template type.
*/
export type SetTemplate = {
id: string;
name: string;
releaseDate: string;
}
/**
* Simplest type of an entry. All card types need to match at least the
* required fields of this template type.
*/
export type EntryTemplate = {
id?: number;
name: string;
language: string;
amount: number;
condition: string;
set: SetTemplate;
setNo?: string;
images: string[];
note: string;
signed: boolean;
altered: boolean;
}
@@ -0,0 +1,25 @@
export type Set = {
id: string;
name: string;
releaseDate: string;
}
export type CardEntry = {
id: number;
name: string;
set: Set;
setNo: string;
language: string;
condition: string;
amount: number;
note: string;
images: string[];
foil: boolean;
signed: boolean;
altered: boolean;
}
@@ -0,0 +1,26 @@
export type Set = {
id: string;
name: string;
releaseDate: string;
}
export type CardEntry = {
id: number;
name: string;
set: Set;
setNo: string;
language: string;
condition: string;
amount: number;
note: string;
images: string[];
firstEdition: boolean;
holo: boolean;
signed: boolean;
altered: boolean;
}