2026-01-17 09:54:47 -05:00
|
|
|
'use client'
|
2026-01-17 15:55:26 -05:00
|
|
|
import React, { SyntheticEvent } from "react";
|
2026-01-17 09:54:47 -05:00
|
|
|
import styles from "@/styles/dev-tools.module.css"
|
2026-01-17 15:55:26 -05:00
|
|
|
import { Container, Select, Stack, Chip, MenuItem, InputLabel, Snackbar, IconButton, SnackbarCloseReason, Typography } from "@mui/material"
|
|
|
|
|
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
|
|
|
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
|
|
|
import Draggable from 'react-draggable';
|
|
|
|
|
|
|
|
|
|
type EditorMode = "insert" | "update";
|
|
|
|
|
|
|
|
|
|
interface DBObject {
|
|
|
|
|
record_num: number
|
|
|
|
|
data_id: string;
|
|
|
|
|
change_timestamp: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Unit extends DBObject {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Card extends DBObject {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EditorContextType {
|
|
|
|
|
editorMode: "insert" | "update";
|
|
|
|
|
currentRecordNum: number | null;
|
|
|
|
|
currentDataId: string | null;
|
|
|
|
|
cards: Card[] | null;
|
|
|
|
|
units: Unit[] | null;
|
|
|
|
|
error: string | null,
|
|
|
|
|
|
|
|
|
|
changeEditorMode?: (m: string) => void;
|
|
|
|
|
changeCurrentRecordNum?: (n: number) => void;
|
|
|
|
|
changeCurrentDataId?: (n: string) => void;
|
|
|
|
|
changeError?: (e: string) => void;
|
|
|
|
|
}
|
|
|
|
|
const EditorContext = React.createContext<EditorContextType>({
|
|
|
|
|
currentRecordNum: null,
|
|
|
|
|
currentDataId: null,
|
|
|
|
|
editorMode: "insert",
|
|
|
|
|
cards: null,
|
|
|
|
|
units: null,
|
|
|
|
|
error: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function useEditor() {
|
|
|
|
|
const ctx = React.useContext(EditorContext);
|
|
|
|
|
if(!ctx){
|
|
|
|
|
throw new Error("Error: use Editor must be used within an EditorContext")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ctx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const unitTypes: string[] = ["minion", "enemy", "hero", "ally"];
|
|
|
|
|
const buildingTypes: string[] = ["enemy", "minion", "terrain", "support"];
|
|
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
export default function Page(){
|
2026-01-17 15:55:26 -05:00
|
|
|
const [currentRecordNum, setCurrentRecordNum] = React.useState<number>(0);
|
|
|
|
|
const [currentDataId, setCurrentDataId] = React.useState<string>("");
|
|
|
|
|
const [editorMode, setEditorMode] = React.useState<EditorMode>("insert");
|
|
|
|
|
const [cards, setCards] = React.useState<Card[] | null>(null);
|
|
|
|
|
const [units, setUnits] = React.useState<Unit[] | null>(null);
|
|
|
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
|
const [open, setOpen] = React.useState<boolean>(false);
|
|
|
|
|
|
|
|
|
|
const changeCurrentRecordNum = (n: number) => setCurrentRecordNum(n)
|
|
|
|
|
const changeCurrentDataId = (id: string) => setCurrentDataId(id)
|
|
|
|
|
const changeEditorMode = (m: string) =>{
|
|
|
|
|
if(m !== "insert" && m !== "update") return
|
|
|
|
|
setEditorMode(m)
|
|
|
|
|
}
|
|
|
|
|
const changeError = (e: string) => setError(e);
|
|
|
|
|
// get all of the cards, or units
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const getCards = async () => {
|
|
|
|
|
try{
|
|
|
|
|
// fetch our db
|
|
|
|
|
}catch (error) {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getUnits = async () => {
|
|
|
|
|
try{
|
|
|
|
|
|
|
|
|
|
}catch (error) {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
getCards();
|
|
|
|
|
getUnits();
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleClose = (
|
|
|
|
|
event: React.SyntheticEvent | Event,
|
|
|
|
|
reason?: SnackbarCloseReason,
|
|
|
|
|
) => {
|
|
|
|
|
if (reason === 'clickaway') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const action = (
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<Typography>
|
|
|
|
|
{error}
|
|
|
|
|
</Typography>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="small"
|
|
|
|
|
aria-label="close"
|
|
|
|
|
color="inherit"
|
|
|
|
|
onClick={handleClose}
|
|
|
|
|
>
|
|
|
|
|
<CloseIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
|
|
|
|
|
return (
|
2026-01-17 15:55:26 -05:00
|
|
|
<EditorContext.Provider value={{
|
|
|
|
|
currentRecordNum,
|
|
|
|
|
currentDataId,
|
|
|
|
|
editorMode,
|
|
|
|
|
error,
|
|
|
|
|
cards,
|
|
|
|
|
units,
|
|
|
|
|
|
|
|
|
|
changeCurrentRecordNum,
|
|
|
|
|
changeCurrentDataId,
|
|
|
|
|
changeEditorMode,
|
|
|
|
|
changeError
|
|
|
|
|
}}>
|
|
|
|
|
<div className={styles.maincontainer}>
|
|
|
|
|
<div className={styles.leftmenucontainer}>
|
|
|
|
|
<LeftMenu />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.editorcontainer}>
|
|
|
|
|
<Editor />
|
|
|
|
|
</div>
|
2026-01-17 09:54:47 -05:00
|
|
|
</div>
|
2026-01-17 15:55:26 -05:00
|
|
|
{error && <Snackbar
|
|
|
|
|
open={open}
|
|
|
|
|
autoHideDuration={6000}
|
|
|
|
|
onClose={handleClose}
|
|
|
|
|
action={action}
|
|
|
|
|
/>}
|
|
|
|
|
</EditorContext.Provider>
|
2026-01-17 09:54:47 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DynamicCard() {
|
|
|
|
|
// to add a new kind of data type "map", "string", "bool", etc. they add a prop for it.
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-01-17 15:55:26 -05:00
|
|
|
interface ThemeSelection {
|
2026-01-17 09:54:47 -05:00
|
|
|
value: string;
|
|
|
|
|
label: string;
|
|
|
|
|
}
|
2026-01-17 15:55:26 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
const debugCard = [
|
|
|
|
|
{
|
|
|
|
|
id: "terrain_forest",
|
|
|
|
|
label: "Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: "terrain_haunted_forest",
|
|
|
|
|
label: "Haunted Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: 'terrain_undead_candy_forest',
|
|
|
|
|
label: "Undead Candy Forest"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "support_blacksmith",
|
|
|
|
|
label: "Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: "support_terrain_haunted_forest",
|
|
|
|
|
label: "Haunted Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: 'SUppror_terrain_undead_candy_forest',
|
|
|
|
|
label: "Undead Candy Forest"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "enemy_graveyard",
|
|
|
|
|
label: "Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: "enemy_terrain_haunted_forest",
|
|
|
|
|
label: "Haunted Forest",
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
id: 'enemy_terrain_undead_candy_forest',
|
|
|
|
|
label: "Undead Candy Forest"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
function LeftMenu(){
|
2026-01-17 15:55:26 -05:00
|
|
|
|
|
|
|
|
const [themes, setThemes] = React.useState<ThemeSelection[]>([]); // get from the database
|
2026-01-17 09:54:47 -05:00
|
|
|
const [menuMode, setMenuMode] = React.useState<"unit" | "card">("card");
|
2026-01-17 15:55:26 -05:00
|
|
|
|
|
|
|
|
const { currentRecordNum, } = useEditor();
|
|
|
|
|
|
|
|
|
|
const handleSelectAsset = (e: React.MouseEvent<Element, MouseEvent>) => {
|
|
|
|
|
console.log("Got e:", e)
|
|
|
|
|
}
|
2026-01-17 09:54:47 -05:00
|
|
|
return (
|
|
|
|
|
<Container className={styles.leftmenu}>
|
2026-01-17 15:55:26 -05:00
|
|
|
<Stack >
|
|
|
|
|
<Typography id="assetTypeFilter">Asset Type</Typography>
|
|
|
|
|
<Select className="assetTypeFilter">
|
|
|
|
|
<MenuItem value="">All</MenuItem>
|
2026-01-17 09:54:47 -05:00
|
|
|
{menuMode === "card" ?
|
2026-01-17 15:55:26 -05:00
|
|
|
buildingTypes.map((opt, i) => (<MenuItem value={opt} key={i}>{opt[0].toUpperCase() + opt.slice(1)}</MenuItem>)) :
|
|
|
|
|
unitTypes.map((opt, i) => (<MenuItem value={opt} key={i}>{opt[0].toUpperCase() + opt.slice(1)}</MenuItem>))
|
2026-01-17 09:54:47 -05:00
|
|
|
}
|
|
|
|
|
</Select>
|
|
|
|
|
|
2026-01-17 15:55:26 -05:00
|
|
|
<InputLabel id="themeFilter">Theme</InputLabel>
|
|
|
|
|
<Select className="themeFilter" label="Theme" >
|
|
|
|
|
<MenuItem value="">All</MenuItem>
|
2026-01-17 09:54:47 -05:00
|
|
|
</Select>
|
2026-01-17 15:55:26 -05:00
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
</Stack>
|
2026-01-17 15:55:26 -05:00
|
|
|
<Typography >
|
|
|
|
|
Current {menuMode}s created
|
|
|
|
|
</Typography>
|
|
|
|
|
<RichTreeView items={debugCard} onItemClick={handleSelectAsset}>
|
|
|
|
|
</RichTreeView>
|
2026-01-17 09:54:47 -05:00
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function Editor(){
|
2026-01-17 15:55:26 -05:00
|
|
|
const { editorMode } = useEditor();
|
|
|
|
|
|
|
|
|
|
const canvasWrapperRef = React.useRef<HTMLDivElement | null>(null);
|
|
|
|
|
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
|
|
|
|
|
|
|
|
|
|
const onMouseUp = (e: React.MouseEvent) => {
|
|
|
|
|
|
|
|
|
|
console.log("event for mouse up", e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMouseDown = (e: React.MouseEvent) => {
|
|
|
|
|
|
|
|
|
|
console.log("event for mouse down", e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDragOver = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
//console.log("event for drag over:", e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onDrop = (e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const raw = e.dataTransfer.getData("text/plain");
|
|
|
|
|
console.log("DROP canvas raw:", raw);
|
|
|
|
|
// create a box?
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
return (
|
|
|
|
|
<Container className={styles.editor} maxWidth={false} disableGutters>
|
2026-01-17 15:55:26 -05:00
|
|
|
|
|
|
|
|
{editorMode === "insert" ? <CreateNewAsset type="building"/> : null}
|
|
|
|
|
<div
|
|
|
|
|
ref={canvasWrapperRef}
|
|
|
|
|
onDragOver={(e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.dataTransfer.dropEffect = "copy";
|
|
|
|
|
}}
|
|
|
|
|
onDrop={(e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const raw = e.dataTransfer.getData("text/plain");
|
|
|
|
|
console.log("DROP wrapper raw:", raw);
|
|
|
|
|
}}
|
|
|
|
|
style={{ height: "100%", width: "100%", position: "relative" }}
|
|
|
|
|
>
|
|
|
|
|
<canvas
|
|
|
|
|
ref={canvasRef}
|
|
|
|
|
onDragOver={onDragOver}
|
|
|
|
|
onDrop={onDrop}
|
|
|
|
|
style={{ height: "100%", width: "100%" }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-17 09:54:47 -05:00
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-17 15:55:26 -05:00
|
|
|
|
|
|
|
|
function CreateNewAsset({ type }: { type: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ backgroundColor: "#F1E9DB", borderRadius: 20, height: 40, display: "flex", alignItems: "center", justifyContent: "center", columnGap: 20 }}>
|
|
|
|
|
{type === "building"
|
|
|
|
|
? buildingTypes.map((buildingType, i) => {
|
|
|
|
|
const label = buildingType[0].toUpperCase() + buildingType.slice(1);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => {
|
|
|
|
|
console.log("DRAG START", buildingType);
|
|
|
|
|
const payload = { kind: "building", type: buildingType, label, w: 160, h: 90 };
|
|
|
|
|
e.dataTransfer.setData("text/plain", JSON.stringify(payload));
|
|
|
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
|
|
|
}}
|
|
|
|
|
style={{ display: "inline-flex" }}
|
|
|
|
|
>
|
|
|
|
|
<Chip sx={{ backgroundColor: "#5DB7DE" }} label={label} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
: unitTypes.map((unitType, i) => {
|
|
|
|
|
const label = unitType[0].toUpperCase() + unitType.slice(1);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => {
|
|
|
|
|
const payload = { kind: "unit", type: unitType, label, w: 140, h: 80 };
|
|
|
|
|
e.dataTransfer.setData("text/plain", JSON.stringify(payload));
|
|
|
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
|
|
|
}}
|
|
|
|
|
style={{ display: "inline-flex" }}
|
|
|
|
|
>
|
|
|
|
|
<Chip sx={{ backgroundColor: "#5DB7DE" }} label={label} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|