mirror of
https://github.com/sebastiandine/Card-Collection-Manager-2.git
synced 2026-09-04 17:23:29 +00:00
initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user