From d4559720f8c2ed3487c3e49df4cad3ed7bb3e009 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Wed, 11 Feb 2026 15:25:29 -0800 Subject: [PATCH] hackdays: Factory Game Scaffold --- src/components/Factory/Canvas/GameCanvas.tsx | 86 +++++++++++++++++++ .../Factory/Canvas/GameControls.tsx | 66 ++++++++++++++ src/components/Factory/FactoryGame.tsx | 54 ++++++++++++ src/components/Factory/Sidebar/Buildings.tsx | 5 ++ src/components/Factory/Sidebar/Controls.tsx | 5 ++ .../Factory/Sidebar/GameSidebar.tsx | 40 +++++++++ src/components/Factory/Sidebar/Resources.tsx | 5 ++ src/components/layout/AppMenu.tsx | 14 ++- src/routes/Factory/FactoryGame.tsx | 18 ++++ src/routes/router.ts | 9 ++ 10 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/components/Factory/Canvas/GameCanvas.tsx create mode 100644 src/components/Factory/Canvas/GameControls.tsx create mode 100644 src/components/Factory/FactoryGame.tsx create mode 100644 src/components/Factory/Sidebar/Buildings.tsx create mode 100644 src/components/Factory/Sidebar/Controls.tsx create mode 100644 src/components/Factory/Sidebar/GameSidebar.tsx create mode 100644 src/components/Factory/Sidebar/Resources.tsx create mode 100644 src/routes/Factory/FactoryGame.tsx diff --git a/src/components/Factory/Canvas/GameCanvas.tsx b/src/components/Factory/Canvas/GameCanvas.tsx new file mode 100644 index 000000000..666f12538 --- /dev/null +++ b/src/components/Factory/Canvas/GameCanvas.tsx @@ -0,0 +1,86 @@ +import { + type Connection, + type Edge, + type Node, + type OnInit, + ReactFlow, + type ReactFlowInstance, + type ReactFlowProps, + useEdgesState, + useNodesState, +} from "@xyflow/react"; +import type { ComponentType } from "react"; +import { useState } from "react"; + +import { BlockStack } from "@/components/ui/layout"; + +const nodeTypes: Record> = { + building: () => ( +
+
Building
+
+ ), +}; + +const edgeTypes: Record> = { + resourceEdge: () => null, // Will use default edge for now +}; + +const GameCanvas = ({ children, ...rest }: ReactFlowProps) => { + const [nodes, , onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [, setReactFlowInstance] = useState(); + + const onInit: OnInit = (instance) => { + setReactFlowInstance(instance); + instance.fitView({ maxZoom: 1 }); + }; + + const onConnect = (connection: Connection) => { + if (connection.source === connection.target) return; + + setEdges((eds) => [ + ...eds, + { + ...connection, + id: `${connection.source}-${connection.target}`, + type: "resourceEdge", + } as Edge, + ]); + }; + + const onNodesDelete = (deleted: Node[]) => { + console.log("Nodes deleted:", deleted); + }; + + const onEdgesDelete = (deleted: Edge[]) => { + console.log("Edges deleted:", deleted); + }; + + return ( + + + {children} + + + ); +}; + +export default GameCanvas; diff --git a/src/components/Factory/Canvas/GameControls.tsx b/src/components/Factory/Canvas/GameControls.tsx new file mode 100644 index 000000000..dea742ebb --- /dev/null +++ b/src/components/Factory/Canvas/GameControls.tsx @@ -0,0 +1,66 @@ +import { + ControlButton, + type ControlProps, + Controls, + type ReactFlowProps, +} from "@xyflow/react"; +import { + LockKeyhole, + LockKeyholeOpen, + SquareDashedMousePointerIcon, +} from "lucide-react"; +import { useCallback, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface GameControlsProps extends ControlProps { + config: ReactFlowProps; + updateConfig: (config: Partial) => void; +} + +export default function GameControls({ + config, + updateConfig, + ...props +}: GameControlsProps) { + const [multiSelectActive, setMultiSelectActive] = useState(false); + const [lockActive, setLockActive] = useState(!config.nodesDraggable); + + const onClickMultiSelect = useCallback(() => { + updateConfig({ + selectionOnDrag: !multiSelectActive, + panOnDrag: multiSelectActive, + }); + setMultiSelectActive(!multiSelectActive); + }, [multiSelectActive, updateConfig]); + + const handleLockChange = useCallback(() => { + updateConfig({ + nodesDraggable: lockActive, + }); + setLockActive(!lockActive); + }, [lockActive, updateConfig]); + + return ( + + {!props.showInteractive && ( + + {lockActive ? ( + + ) : ( + + )} + + )} + + + + + ); +} diff --git a/src/components/Factory/FactoryGame.tsx b/src/components/Factory/FactoryGame.tsx new file mode 100644 index 000000000..2850fa62d --- /dev/null +++ b/src/components/Factory/FactoryGame.tsx @@ -0,0 +1,54 @@ +import "@/styles/editor.css"; + +import { Background, MiniMap, type ReactFlowProps } from "@xyflow/react"; +import { useState } from "react"; + +import { CollapsibleContextPanel } from "@/components/shared/ContextPanel/CollapsibleContextPanel"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; + +import GameCanvas from "./Canvas/GameCanvas"; +import GameControls from "./Canvas/GameControls"; +import GameSidebar from "./Sidebar/GameSidebar"; + +const GRID_SIZE = 10; + +const FactoryGame = () => { + const [flowConfig, setFlowConfig] = useState({ + snapGrid: [GRID_SIZE, GRID_SIZE], + snapToGrid: true, + panOnDrag: true, + selectionOnDrag: false, + nodesDraggable: true, + }); + + const updateFlowConfig = (updatedConfig: Partial) => { + setFlowConfig((prevConfig) => ({ + ...prevConfig, + ...updatedConfig, + })); + }; + + return ( + Factory Game

}> + + + + + + + + + + + +
+ ); +}; + +export default FactoryGame; diff --git a/src/components/Factory/Sidebar/Buildings.tsx b/src/components/Factory/Sidebar/Buildings.tsx new file mode 100644 index 000000000..25d0d66ed --- /dev/null +++ b/src/components/Factory/Sidebar/Buildings.tsx @@ -0,0 +1,5 @@ +const Buildings = () => { + return

Building List

; +}; + +export default Buildings; diff --git a/src/components/Factory/Sidebar/Controls.tsx b/src/components/Factory/Sidebar/Controls.tsx new file mode 100644 index 000000000..a94e7d252 --- /dev/null +++ b/src/components/Factory/Sidebar/Controls.tsx @@ -0,0 +1,5 @@ +const Controls = () => { + return

Next Turn

; +}; + +export default Controls; diff --git a/src/components/Factory/Sidebar/GameSidebar.tsx b/src/components/Factory/Sidebar/GameSidebar.tsx new file mode 100644 index 000000000..d4dea5250 --- /dev/null +++ b/src/components/Factory/Sidebar/GameSidebar.tsx @@ -0,0 +1,40 @@ +import { BlockStack } from "@/components/ui/layout"; +import { VerticalResizeHandle } from "@/components/ui/resize-handle"; +import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants"; + +import Buildings from "./Buildings"; +import Controls from "./Controls"; +import Resources from "./Resources"; + +const MIN_WIDTH = 220; +const MAX_WIDTH = 400; +const DEFAULT_WIDTH = 256; + +const GameSidebar = () => { + return ( +
+ + + + + + + +
+ ); +}; + +export default GameSidebar; diff --git a/src/components/Factory/Sidebar/Resources.tsx b/src/components/Factory/Sidebar/Resources.tsx new file mode 100644 index 000000000..ece47b9bf --- /dev/null +++ b/src/components/Factory/Sidebar/Resources.tsx @@ -0,0 +1,5 @@ +const Resources = () => { + return

Gold: 0

; +}; + +export default Resources; diff --git a/src/components/layout/AppMenu.tsx b/src/components/layout/AppMenu.tsx index 95fe99450..ba67e25e5 100644 --- a/src/components/layout/AppMenu.tsx +++ b/src/components/layout/AppMenu.tsx @@ -29,9 +29,20 @@ import { PersonalPreferences } from "../shared/Settings/PersonalPreferences"; const AppMenu = () => { const requiresAuthorization = isAuthorizationRequired(); const { componentSpec } = useComponentSpec(); - const title = componentSpec?.name; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const isFactoryGame = window.location.pathname === "/factory"; + + const title = isFactoryGame ? "FACTORY" : componentSpec?.name; + + const gameButton = ( + + + + + + ); + const documentationButton = ( @@ -71,6 +82,7 @@ const AppMenu = () => { {/* Always visible settings */} + {gameButton} diff --git a/src/routes/Factory/FactoryGame.tsx b/src/routes/Factory/FactoryGame.tsx new file mode 100644 index 000000000..9df8387de --- /dev/null +++ b/src/routes/Factory/FactoryGame.tsx @@ -0,0 +1,18 @@ +import "@/styles/editor.css"; + +import { DndContext } from "@dnd-kit/core"; +import { ReactFlowProvider } from "@xyflow/react"; + +import FactoryGame from "@/components/Factory/FactoryGame"; + +const Factory = () => { + return ( + + + + + + ); +}; + +export default Factory; diff --git a/src/routes/router.ts b/src/routes/router.ts index 212152935..0704c8948 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -13,6 +13,7 @@ import { BASE_URL, IS_GITHUB_PAGES } from "@/utils/constants"; import RootLayout from "../components/layout/RootLayout"; import Editor from "./Editor"; +import FactoryGame from "./Factory/FactoryGame"; import Home from "./Home"; import NotFoundPage from "./NotFoundPage"; import PipelineRun from "./PipelineRun"; @@ -36,6 +37,7 @@ export const APP_ROUTES = { RUNS: RUNS_BASE_PATH, GITHUB_AUTH_CALLBACK: "/authorize/github", HUGGINGFACE_AUTH_CALLBACK: "/authorize/huggingface", + FACTORY_GAME: "/factory", }; const rootRoute = createRootRoute({ @@ -96,12 +98,19 @@ const runDetailWithSubgraphRoute = createRoute({ component: PipelineRun, }); +const factoryGameRoute = createRoute({ + getParentRoute: () => mainLayout, + path: APP_ROUTES.FACTORY_GAME, + component: FactoryGame, +}); + const appRouteTree = mainLayout.addChildren([ indexRoute, quickStartRoute, editorRoute, runDetailRoute, runDetailWithSubgraphRoute, + factoryGameRoute, ]); const rootRouteTree = rootRoute.addChildren([