aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui-new/src/pages/instances-view.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui-new/src/pages/instances-view.tsx')
-rw-r--r--packages/ui-new/src/pages/instances-view.tsx370
1 files changed, 370 insertions, 0 deletions
diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui-new/src/pages/instances-view.tsx
new file mode 100644
index 0000000..0c511a1
--- /dev/null
+++ b/packages/ui-new/src/pages/instances-view.tsx
@@ -0,0 +1,370 @@
+import { Copy, Edit2, Plus, Trash2 } from "lucide-react";
+import { useEffect, useState } from "react";
+import InstanceEditorModal from "@/components/instance-editor-modal";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { toNumber } from "@/lib/tsrs-utils";
+import { useInstancesStore } from "@/stores/instances-store";
+import type { Instance } from "../types/bindings/instance";
+
+export function InstancesView() {
+ const instancesStore = useInstancesStore();
+
+ // Modal / UI state
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [showDuplicateModal, setShowDuplicateModal] = useState(false);
+
+ // Selected / editing instance state
+ const [selectedInstance, setSelectedInstance] = useState<Instance | null>(
+ null,
+ );
+ const [editingInstance, setEditingInstance] = useState<Instance | null>(null);
+
+ // Form fields
+ const [newInstanceName, setNewInstanceName] = useState("");
+ const [duplicateName, setDuplicateName] = useState("");
+
+ // Load instances on mount (matches Svelte onMount behavior)
+ useEffect(() => {
+ instancesStore.loadInstances();
+ // instancesStore methods are stable (Zustand); do not add to deps to avoid spurious runs
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [instancesStore.loadInstances]);
+
+ // Handlers to open modals
+ const openCreate = () => {
+ setNewInstanceName("");
+ setShowCreateModal(true);
+ };
+
+ const openEdit = (instance: Instance) => {
+ setEditingInstance({ ...instance });
+ setShowEditModal(true);
+ };
+
+ const openDelete = (instance: Instance) => {
+ setSelectedInstance(instance);
+ setShowDeleteConfirm(true);
+ };
+
+ const openDuplicate = (instance: Instance) => {
+ setSelectedInstance(instance);
+ setDuplicateName(`${instance.name} (Copy)`);
+ setShowDuplicateModal(true);
+ };
+
+ // Confirm actions
+ const confirmCreate = async () => {
+ const name = newInstanceName.trim();
+ if (!name) return;
+ await instancesStore.createInstance(name);
+ setShowCreateModal(false);
+ setNewInstanceName("");
+ };
+
+ const confirmEdit = async () => {
+ if (!editingInstance) return;
+ await instancesStore.updateInstance(editingInstance);
+ setEditingInstance(null);
+ setShowEditModal(false);
+ };
+
+ const confirmDelete = async () => {
+ if (!selectedInstance) return;
+ await instancesStore.deleteInstance(selectedInstance.id);
+ setSelectedInstance(null);
+ setShowDeleteConfirm(false);
+ };
+
+ const confirmDuplicate = async () => {
+ if (!selectedInstance) return;
+ const name = duplicateName.trim();
+ if (!name) return;
+ await instancesStore.duplicateInstance(selectedInstance.id, name);
+ setSelectedInstance(null);
+ setDuplicateName("");
+ setShowDuplicateModal(false);
+ };
+
+ const setActiveInstance = async (id: string) => {
+ await instancesStore.setActiveInstance(id);
+ };
+
+ const formatDate = (timestamp: number): string =>
+ new Date(timestamp * 1000).toLocaleDateString();
+
+ const formatLastPlayed = (timestamp: number): string => {
+ const date = new Date(timestamp * 1000);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+ if (days === 0) return "Today";
+ if (days === 1) return "Yesterday";
+ if (days < 7) return `${days} days ago`;
+ return date.toLocaleDateString();
+ };
+
+ return (
+ <div className="h-full flex flex-col gap-4 p-6 overflow-y-auto">
+ <div className="flex items-center justify-between">
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
+ Instances
+ </h1>
+ <Button
+ type="button"
+ onClick={openCreate}
+ className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
+ >
+ <Plus size={18} />
+ Create Instance
+ </Button>
+ </div>
+
+ {instancesStore.instances.length === 0 ? (
+ <div className="flex-1 flex items-center justify-center">
+ <div className="text-center text-gray-500 dark:text-gray-400">
+ <p className="text-lg mb-2">No instances yet</p>
+ <p className="text-sm">Create your first instance to get started</p>
+ </div>
+ </div>
+ ) : (
+ <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {instancesStore.instances.map((instance) => {
+ const isActive = instancesStore.activeInstanceId === instance.id;
+
+ return (
+ <li
+ key={instance.id}
+ onClick={() => setActiveInstance(instance.id)}
+ onKeyDown={(e) =>
+ e.key === "Enter" && setActiveInstance(instance.id)
+ }
+ className={`relative p-4 text-left rounded-lg border-2 transition-all cursor-pointer hover:border-blue-500 ${
+ isActive ? "border-blue-500" : "border-transparent"
+ } bg-gray-100 dark:bg-gray-800`}
+ >
+ {/* Instance Icon */}
+ {instance.iconPath ? (
+ <div className="w-12 h-12 mb-3 rounded overflow-hidden">
+ <img
+ src={instance.iconPath}
+ alt={instance.name}
+ className="w-full h-full object-cover"
+ />
+ </div>
+ ) : (
+ <div className="w-12 h-12 mb-3 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center">
+ <span className="text-white font-bold text-lg">
+ {instance.name.charAt(0).toUpperCase()}
+ </span>
+ </div>
+ )}
+
+ <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
+ {instance.name}
+ </h3>
+
+ <div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
+ {instance.versionId ? (
+ <p className="truncate">Version: {instance.versionId}</p>
+ ) : (
+ <p className="text-gray-400">No version selected</p>
+ )}
+
+ {instance.modLoader && (
+ <p className="truncate">
+ Mod Loader:{" "}
+ <span className="capitalize">{instance.modLoader}</span>
+ </p>
+ )}
+
+ <p className="truncate">
+ Created: {formatDate(toNumber(instance.createdAt))}
+ </p>
+
+ {instance.lastPlayed && (
+ <p className="truncate">
+ Last played:{" "}
+ {formatLastPlayed(toNumber(instance.lastPlayed))}
+ </p>
+ )}
+ </div>
+
+ {/* Action Buttons */}
+ <div className="mt-4 flex gap-2">
+ <button
+ type="button"
+ onClick={(e) => {
+ e.stopPropagation();
+ openEdit(instance);
+ }}
+ className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
+ >
+ <Edit2 size={14} />
+ Edit
+ </button>
+
+ <button
+ type="button"
+ onClick={(e) => {
+ e.stopPropagation();
+ openDuplicate(instance);
+ }}
+ className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors"
+ >
+ <Copy size={14} />
+ Duplicate
+ </button>
+
+ <button
+ type="button"
+ onClick={(e) => {
+ e.stopPropagation();
+ openDelete(instance);
+ }}
+ className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors"
+ >
+ <Trash2 size={14} />
+ Delete
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+
+ {/* Create Modal */}
+ <Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Create Instance</DialogTitle>
+ <DialogDescription>
+ Enter a name for the new instance.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="mt-4">
+ <Input
+ value={newInstanceName}
+ onChange={(e) => setNewInstanceName(e.target.value)}
+ placeholder="Instance name"
+ />
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setShowCreateModal(false)}
+ >
+ Cancel
+ </Button>
+ <Button
+ type="button"
+ onClick={confirmCreate}
+ disabled={!newInstanceName.trim()}
+ >
+ Create
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ <InstanceEditorModal
+ open={showEditModal}
+ instance={editingInstance}
+ onOpenChange={(open) => {
+ setShowEditModal(open);
+ if (!open) setEditingInstance(null);
+ }}
+ />
+
+ {/* Delete Confirmation */}
+ <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Delete Instance</DialogTitle>
+ <DialogDescription>
+ Are you sure you want to delete "{selectedInstance?.name}"? This
+ action cannot be undone.
+ </DialogDescription>
+ </DialogHeader>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => {
+ setShowDeleteConfirm(false);
+ setSelectedInstance(null);
+ }}
+ >
+ Cancel
+ </Button>
+ <Button
+ type="button"
+ onClick={confirmDelete}
+ className="bg-red-600 text-white hover:bg-red-500"
+ >
+ Delete
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* Duplicate Modal */}
+ <Dialog open={showDuplicateModal} onOpenChange={setShowDuplicateModal}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Duplicate Instance</DialogTitle>
+ <DialogDescription>
+ Provide a name for the duplicated instance.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="mt-4">
+ <Input
+ value={duplicateName}
+ onChange={(e) => setDuplicateName(e.target.value)}
+ placeholder="New instance name"
+ onKeyDown={(e) => e.key === "Enter" && confirmDuplicate()}
+ />
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => {
+ setShowDuplicateModal(false);
+ setSelectedInstance(null);
+ setDuplicateName("");
+ }}
+ >
+ Cancel
+ </Button>
+ <Button
+ type="button"
+ onClick={confirmDuplicate}
+ disabled={!duplicateName.trim()}
+ >
+ Duplicate
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+ );
+}