Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/components/Factory/Canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
@@ -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<string, ComponentType<any>> = {
building: () => (
<div className="px-4 py-2 shadow-md rounded-md bg-white border-2 border-stone-400">
<div className="font-bold">Building</div>
</div>
),
};

const edgeTypes: Record<string, ComponentType<any>> = {
resourceEdge: () => null, // Will use default edge for now
};

const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
const [nodes, , onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
const [, setReactFlowInstance] = useState<ReactFlowInstance>();

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 (
<BlockStack fill className="relative">
<ReactFlow
{...rest}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onInit={onInit}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
minZoom={0.1}
maxZoom={2}
deleteKeyCode={["Delete", "Backspace"]}
proOptions={{ hideAttribution: true }}
fitView
>
{children}
</ReactFlow>
</BlockStack>
);
};

export default GameCanvas;
66 changes: 66 additions & 0 deletions src/components/Factory/Canvas/GameControls.tsx
Original file line number Diff line number Diff line change
@@ -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<ReactFlowProps>) => 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 (
<Controls {...props}>
{!props.showInteractive && (
<ControlButton
onClick={handleLockChange}
className={cn(lockActive && "bg-gray-100!")}
>
{lockActive ? (
<LockKeyhole className="fill-none! -scale-x-120 scale-y-120" />
) : (
<LockKeyholeOpen className="fill-none! -scale-x-120 scale-y-120" />
)}
</ControlButton>
)}
<ControlButton
onClick={onClickMultiSelect}
className={cn(multiSelectActive && "bg-gray-100!")}
>
<SquareDashedMousePointerIcon className="scale-120" />
</ControlButton>
</Controls>
);
}
54 changes: 54 additions & 0 deletions src/components/Factory/FactoryGame.tsx
Original file line number Diff line number Diff line change
@@ -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<ReactFlowProps>({
snapGrid: [GRID_SIZE, GRID_SIZE],
snapToGrid: true,
panOnDrag: true,
selectionOnDrag: false,
nodesDraggable: true,
});

const updateFlowConfig = (updatedConfig: Partial<ReactFlowProps>) => {
setFlowConfig((prevConfig) => ({
...prevConfig,
...updatedConfig,
}));
};

return (
<ContextPanelProvider defaultContent={<p>Factory Game</p>}>
<InlineStack fill>
<GameSidebar />
<BlockStack fill className="flex-1 relative">
<GameCanvas {...flowConfig}>
<MiniMap position="bottom-left" pannable />
<GameControls
className="ml-56! mb-6!"
config={flowConfig}
updateConfig={updateFlowConfig}
showInteractive={false}
/>
<Background gap={GRID_SIZE} className="bg-slate-50!" />
</GameCanvas>
</BlockStack>
<CollapsibleContextPanel />
</InlineStack>
</ContextPanelProvider>
);
};

export default FactoryGame;
5 changes: 5 additions & 0 deletions src/components/Factory/Sidebar/Buildings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Buildings = () => {
return <p>Building List</p>;
};

export default Buildings;
5 changes: 5 additions & 0 deletions src/components/Factory/Sidebar/Controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Controls = () => {
return <p>Next Turn</p>;
};

export default Controls;
40 changes: 40 additions & 0 deletions src/components/Factory/Sidebar/GameSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="relative h-full bg-sidebar text-sidebar-foreground overflow-x-hidden overflow-y-auto"
data-testid="flow-sidebar-container"
style={{
width: `${DEFAULT_WIDTH}px`,
minWidth: `${MIN_WIDTH}px`,
maxWidth: `${MAX_WIDTH}px`,
maxHeight: `calc(100vh - ${TOP_NAV_HEIGHT}px - ${BOTTOM_FOOTER_HEIGHT}px)`,
}}
>
<BlockStack fill gap="2" inlineAlign="start" className="p-4">
<Controls />
<Resources />
<Buildings />
</BlockStack>

<VerticalResizeHandle
side="right"
minWidth={MIN_WIDTH}
maxWidth={MAX_WIDTH}
/>
</div>
);
};

export default GameSidebar;
5 changes: 5 additions & 0 deletions src/components/Factory/Sidebar/Resources.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Resources = () => {
return <p>Gold: 0</p>;
};

export default Resources;
14 changes: 13 additions & 1 deletion src/components/layout/AppMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<Link href="/factory" target="_blank" rel="noopener noreferrer">
<TooltipButton tooltip="Factory Game">
<Icon name="Gamepad2" />
</TooltipButton>
</Link>
);

const documentationButton = (
<Link href={DOCUMENTATION_URL} target="_blank" rel="noopener noreferrer">
<TooltipButton tooltip="Documentation">
Expand Down Expand Up @@ -71,6 +82,7 @@ const AppMenu = () => {

{/* Always visible settings */}
<InlineStack gap="2" wrap="nowrap">
{gameButton}
<BackendStatus />
<PersonalPreferences />
<ManageSecretsButton />
Expand Down
18 changes: 18 additions & 0 deletions src/routes/Factory/FactoryGame.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DndContext>
<ReactFlowProvider>
<FactoryGame />
</ReactFlowProvider>
</DndContext>
);
};

export default Factory;
9 changes: 9 additions & 0 deletions src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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({
Expand Down Expand Up @@ -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([
Expand Down