organized the dev tools dahsboard componetns and connected them tothe backend slightly. Still working on how to display combos as a tree, then the inspector should follow.

This commit is contained in:
HP
2026-01-19 03:06:22 -05:00
parent 9696f6cc33
commit aa4578cc2e
11 changed files with 325 additions and 269 deletions

View File

@@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from "next/server";
import { Pool } from "pg";
const db = new Pool({
connectionString: process.env.DB_STRING!
})
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ data_id: string }> }
){
try{
const { data_id } = await params;
if(!data_id){
return NextResponse.json({ message: "Invalid search parameter:" + data_id }, { status: 400, statusText: "Malformed request" })
}
const combosRes = await db.query(
`WITH RECURSIVE t AS (
SELECT parent_data_id, combo_data_id, result_data_id, 1 AS depth
FROM combos
WHERE parent_data_id = $1
UNION ALL
SELECT c.parent_data_id, c.combo_data_id, c.result_data_id, t.depth + 1
FROM combos c
JOIN t on c.parent_data_id = t.result_data_id
WHERE t.depth < 3
)
SELECT * FROM t ORDER BY depth, parent_data_id, combo_data_id;`, [data_id]
);
return NextResponse.json(combosRes.rows ?? [])
}catch(error){
return NextResponse.json({ message: "Could not GET combos: " + error }, { status: 500, statusText: "Serverside exception" })
}
}

View File

@@ -0,0 +1,18 @@
import { NextResponse } from "next/server"
import { Pool } from "pg"
const db = new Pool({
connectionString: process.env.DB_STRING!
})
export async function GET(){
try{
const result = await db.query("SELECT * FROM cards;")
console.log("Got some combos")
return NextResponse.json(result.rows ?? [])
}catch(err){
console.error("Could not GET cards:", err)
return NextResponse.json( { message: "Serverside exception occurred " + err }, { status: 500 } )
}
}

View File

@@ -19,8 +19,7 @@ export async function GET(req: NextRequest){
export async function POST(req: NextRequest){ export async function POST(req: NextRequest){
try{ try{
const body = (await req.json()) as { name: string, expiration_date: string } const { name, expiration_date } = (await req.json()) as { name: string, expiration_date: string }
console.log("GOT BODY:", body)
const result = await db.query("INSERT INTO products (name, exp_date) VALUES ($1, $2) RETURNING record_num;", [name, expiration_date]); const result = await db.query("INSERT INTO products (name, exp_date) VALUES ($1, $2) RETURNING record_num;", [name, expiration_date]);
return NextResponse.json(result.rows[0].record_num ?? []); return NextResponse.json(result.rows[0].record_num ?? []);
} catch(error){ } catch(error){

View File

@@ -1,67 +1,20 @@
'use client' 'use client'
import React, { SyntheticEvent } from "react"; import React from "react";
import styles from "@/styles/dev-tools.module.css" import styles from "@/styles/dev-tools.module.css"
import { Container, Select, Stack, Chip, MenuItem, InputLabel, Snackbar, IconButton, SnackbarCloseReason, Typography } from "@mui/material" import { IconButton, Snackbar, SnackbarCloseReason, Typography } from "@mui/material"
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import Draggable from 'react-draggable'; import { Card, Combo, EditorMode, Unit } from "@/types/dev-tools";
import { EditorContext } from "@/contexts/EditorContext";
type EditorMode = "insert" | "update"; import LeftMenu from "@/components/dev-tools/LeftMenu";
import { useEditor } from "@/hooks/useEditor";
interface DBObject { import Editor from "@/components/dev-tools/Editor";
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"];
export default function Page(){ export default function Page(){
const [currentRecordNum, setCurrentRecordNum] = React.useState<number>(0); const [currentRecordNum, setCurrentRecordNum] = React.useState<number>(0);
const [currentDataId, setCurrentDataId] = React.useState<string>(""); const [currentDataId, setCurrentDataId] = React.useState<string>("");
const [editorMode, setEditorMode] = React.useState<EditorMode>("insert"); const [editorMode, setEditorMode] = React.useState<EditorMode>("insert");
const [cards, setCards] = React.useState<Card[] | null>(null); const [cards, setCards] = React.useState<Card[] | null>(null);
const [currentCombos, setCurrentCombos] = React.useState<Combo[] | null>(null);
const [units, setUnits] = React.useState<Unit[] | null>(null); const [units, setUnits] = React.useState<Unit[] | null>(null);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const [open, setOpen] = React.useState<boolean>(false); const [open, setOpen] = React.useState<boolean>(false);
@@ -73,27 +26,70 @@ export default function Page(){
setEditorMode(m) setEditorMode(m)
} }
const changeError = (e: string) => setError(e); const changeError = (e: string) => setError(e);
// get all of the cards, or units
React.useEffect(() => { React.useEffect(() => {
const getCards = async () => { const getCards = async () => {
try{ try{
// fetch our db // fetch our db
const res = await fetch("/api/dev/vor/cards", {
method: "GET",
})
const body = await res.json()
if(!res.ok){
console.error("Could not get vor cards:", body)
setCards(null);
} else{
setCards(body)
}
}catch (error) { }catch (error) {
console.error("Could not get vor cards:", error)
setCards(null);
} }
} }
const getUnits = async () => { const getUnits = async () => {
try{ try{
// fetch our db
const res = await fetch("/api/dev/vor/units", {
method: "GET",
})
const body = await res.json()
if(!res.ok){
console.error("Could not get vor units:", body)
setUnits(null);
} else{
setUnits(body)
}
}catch (error) { }catch (error) {
console.error("Could not get vor units:", error)
setUnits(null);
} }
} }
getCards(); getCards();
getUnits(); getUnits();
}, []) }, [])
React.useEffect(() => {
const getCombos = async () => {
// first check if the user has unconfirmed changes, if not then go for the query
try{
const res = await fetch(`/api/dev/vor/${currentDataId}/combos`, {
method: "GET",
})
const body = await res.json();
if(res.ok){
setCurrentCombos(body)
}else{
setCurrentCombos(null)
console.error("Failed to get combos for:", body)
}
} catch(error){
setCurrentCombos(null)
console.error("Could not get combos for:", error)
}
}
},[currentDataId])
const handleClose = ( const handleClose = (
event: React.SyntheticEvent | Event, event: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason, reason?: SnackbarCloseReason,
@@ -130,6 +126,7 @@ export default function Page(){
error, error,
cards, cards,
units, units,
currentCombos,
changeCurrentRecordNum, changeCurrentRecordNum,
changeCurrentDataId, changeCurrentDataId,
@@ -154,208 +151,3 @@ export default function Page(){
) )
} }
function DynamicCard() {
// to add a new kind of data type "map", "string", "bool", etc. they add a prop for it.
return (
<>
</>
)
}
interface ThemeSelection {
value: string;
label: string;
}
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"
}
]
}
]
}
]
function LeftMenu(){
const [themes, setThemes] = React.useState<ThemeSelection[]>([]); // get from the database
const [menuMode, setMenuMode] = React.useState<"unit" | "card">("card");
const { currentRecordNum, } = useEditor();
const handleSelectAsset = (e: React.MouseEvent<Element, MouseEvent>) => {
console.log("Got e:", e)
}
return (
<Container className={styles.leftmenu}>
<Stack >
<Typography id="assetTypeFilter">Asset Type</Typography>
<Select className="assetTypeFilter">
<MenuItem value="">All</MenuItem>
{menuMode === "card" ?
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>))
}
</Select>
<InputLabel id="themeFilter">Theme</InputLabel>
<Select className="themeFilter" label="Theme" >
<MenuItem value="">All</MenuItem>
</Select>
</Stack>
<Typography >
Current {menuMode}s created
</Typography>
<RichTreeView items={debugCard} onItemClick={handleSelectAsset}>
</RichTreeView>
</Container>
)
}
function Editor(){
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?
}
return (
<Container className={styles.editor} maxWidth={false} disableGutters>
{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>
</Container>
)
}
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>
);
}

View File

@@ -0,0 +1,63 @@
import React from "react";
import styles from "@/styles/dev-tools.module.css"
import { Container, Chip } from "@mui/material"
import { buildingTypes, unitTypes } from "@/types/dev-tools-extra";
import { useEditor } from "@/hooks/useEditor";
export default function Editor(){
// TODO: create an inspector (in HTML) and a node view (in canvas)
const { editorMode, currentDataId } = useEditor();
const canvasWrapperRef = React.useRef<HTMLDivElement | null>(null);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
return (
<Container className={styles.editor} maxWidth={false} disableGutters>
{editorMode === "insert" ? <CreateNewAsset type="building"/> : null}
<div
ref={canvasWrapperRef}
style={{ height: "100%", width: "100%", position: "relative" }}
>
<canvas
ref={canvasRef}
style={{ height: "100%", width: "100%" }}
/>
</div>
</Container>
)
}
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}
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}
style={{ display: "inline-flex" }}
>
<Chip sx={{ backgroundColor: "#5DB7DE" }} label={label} />
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,62 @@
import React from "react";
import styles from "@/styles/dev-tools.module.css"
import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
import { Container, Select, Stack, MenuItem, InputLabel, Typography } from "@mui/material"
import { buildingTypes, unitTypes } from "@/types/dev-tools-extra";
import { useEditor } from "@/hooks/useEditor";
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"
}
]
}
]
},
]
export default function LeftMenu(){
//const [themes, setThemes] = React.useState<ThemeSelection[]>([]); // get from the database
const [menuMode, setMenuMode] = React.useState<"unit" | "card">("card");
const { changeCurrentDataId } = useEditor();
const handleSelectAsset = (_e: React.MouseEvent<Element, MouseEvent>, id: string) => {
changeCurrentDataId!(id);
}
return (
<Container className={styles.leftmenu}>
<Stack >
<Typography id="assetTypeFilter">Asset Type</Typography>
<Select className="assetTypeFilter">
<MenuItem value="">All</MenuItem>
{menuMode === "card" ?
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>))
}
</Select>
<InputLabel id="themeFilter">Theme</InputLabel>
<Select className="themeFilter" label="Theme" >
<MenuItem value="">All</MenuItem>
</Select>
</Stack>
<Typography >
Current {menuMode}s created
</Typography>
<RichTreeView items={debugCard} onItemClick={handleSelectAsset}>
</RichTreeView>
</Container>
)
}

View File

@@ -0,0 +1,28 @@
import { Card, Unit, Combo } from "@/types/dev-tools"
import { createContext } from "react"
export interface EditorContextType {
editorMode: "insert" | "update";
currentRecordNum: number | null;
currentDataId: string | null;
currentCombos: Combo[] | 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;
}
export const EditorContext = createContext<EditorContextType>({
currentRecordNum: null,
currentDataId: null,
editorMode: "insert",
cards: null,
units: null,
currentCombos: null,
error: null,
})

11
src/hooks/useEditor.ts Normal file
View File

@@ -0,0 +1,11 @@
import { EditorContext } from "@/contexts/EditorContext"
import { useContext } from "react";
export function useEditor() {
const ctx = useContext(EditorContext);
if(!ctx){
throw new Error("Error: use Editor must be used within an EditorContext")
}
return ctx;
}

View File

@@ -16,8 +16,6 @@ CREATE TABLE cards (
created_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP created_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); );
DROP TABLE IF EXISTS units; DROP TABLE IF EXISTS units;
DROP SEQUENCE IF EXISTS units_record_num_sequence; DROP SEQUENCE IF EXISTS units_record_num_sequence;
DROP SEQUENCE IF EXISTS units_unit_id_sequence; DROP SEQUENCE IF EXISTS units_unit_id_sequence;
@@ -42,6 +40,20 @@ CREATE TABLE units (
created_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP created_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); );
DROP TABLE IF EXISTS combos;
DROP SEQUENCE IF EXISTS combos_record_num_sequence;
CREATE SEQUENCE combos_record_num_sequence;
CREATE TABLE combos (
record_num INTEGER PRIMARY KEY DEFAULT nextval('combos_record_num_sequence'),
parent_data_id VARCHAR(100) NOT NULL,
combo_data_id VARCHAR(100) NOT NULL,
result_data_id VARCHAR(100) NOT NULL,
CHECK (parent_data_id <> result_data_id)
);
DROP TABLE IF EXISTS resources; DROP TABLE IF EXISTS resources;
DROP SEQUENCE IF EXISTS resources_record_num_sequence; DROP SEQUENCE IF EXISTS resources_record_num_sequence;
DROP SEQUENCE IF EXISTS resources_resource_id_sequence; DROP SEQUENCE IF EXISTS resources_resource_id_sequence;

View File

@@ -0,0 +1,5 @@
export const unitTypes: string[] = ["minion", "enemy", "hero", "ally"];
export const buildingTypes: string[] = ["enemy", "minion", "terrain", "support"];

28
src/types/dev-tools.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
export type EditorMode = "insert" | "update";
export interface Combo {
parent_data_id: string;
combo_data_id: string;
result_data_id: string;
}
export interface DBObject {
record_num: number
data_id: string;
change_timestamp: string;
}
export interface Unit extends DBObject {
}
export interface Card extends DBObject {
}
export interface ThemeSelection {
value: string;
label: string;
}