initial commit after project creation

This commit is contained in:
Gerhard Scheikl
2026-04-01 09:38:50 +02:00
commit b02af637d4
292 changed files with 61408 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
"use client";
import { DocumentIcon, FolderIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import React from "react";
import { wrapFieldsWithMeta } from "tinacms";
import {
DRAG_STATE_RESET,
type DragState,
type FileItem,
FileTreeItem,
type TreeNode,
} from "./file-structure.item";
// Convert flat array to tree structure
const buildTree = (items: FileItem[]): TreeNode[] => {
const itemMap = new Map<string, TreeNode>();
const rootItems: TreeNode[] = [];
for (const item of items) {
itemMap.set(item.id, {
...item,
children: [],
level: 0,
});
}
for (const item of items) {
const node = itemMap.get(item.id);
if (!node) continue;
if (item.parentId === null) {
rootItems.push(node);
} else {
const parent = itemMap.get(item.parentId);
if (parent) {
parent.children.push(node);
node.level = parent.level + 1;
}
}
}
return rootItems;
};
export const FileStructureField = wrapFieldsWithMeta(({ input }) => {
const items: FileItem[] = input.value || [];
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set()
);
const [editingId, setEditingId] = useState<string | null>(null);
const [dragState, setDragState] = useState<DragState>(DRAG_STATE_RESET);
const toggleFolder = (id: string) => {
setExpandedFolders((prev) =>
prev.has(id)
? new Set([...prev].filter((item) => item !== id))
: new Set(prev).add(id)
);
};
const isValidDrop = (draggedId: string, targetId: string): boolean => {
if (draggedId === targetId) return false;
const draggedItem = items.find((item) => item.id === draggedId);
if (draggedItem?.type !== "folder") return true;
// Check if target is a descendant of dragged folder
const isDescendant = (parentId: string, checkId: string): boolean =>
items.some(
(item) =>
item.parentId === parentId &&
(item.id === checkId || isDescendant(item.id, checkId))
);
return !isDescendant(draggedId, targetId);
};
const moveItem = (
draggedId: string,
targetId: string | null,
position: "before" | "after" | "inside"
) => {
if (!isValidDrop(draggedId, targetId || "")) return;
const draggedItem = items.find((item) => item.id === draggedId);
const targetItem = targetId
? items.find((item) => item.id === targetId)
: null;
if (!draggedItem || !targetItem) return;
const filteredItems = items.filter((item) => item.id !== draggedId);
const targetIndex = filteredItems.findIndex((item) => item.id === targetId);
if (targetIndex === -1) return;
let insertIndex: number;
let newParentId: string | null;
if (position === "inside" && targetItem.type === "folder" && targetId) {
newParentId = targetId;
setExpandedFolders((prev) => new Set(prev).add(targetId));
// Insert after last child or after target folder
const lastChild = filteredItems.findLastIndex(
(item) => item.parentId === targetId
);
insertIndex = lastChild !== -1 ? lastChild + 1 : targetIndex + 1;
} else {
newParentId = targetItem.parentId;
insertIndex = position === "before" ? targetIndex : targetIndex + 1;
}
filteredItems.splice(insertIndex, 0, {
...draggedItem,
parentId: newParentId,
});
input.onChange(filteredItems);
};
const addItem = (parentId: string | null, type: "file" | "folder") => {
const newItem: FileItem = {
id: Math.random().toString(36).substr(2, 9),
name: type === "folder" ? "New Folder" : "new-file.txt",
type,
parentId,
};
input.onChange([...items, newItem]);
setEditingId(newItem.id);
if (parentId) {
setExpandedFolders((prev) => new Set(prev).add(parentId));
}
};
const updateItem = (id: string, updates: Partial<FileItem>) => {
input.onChange(
items.map((item) => (item.id === id ? { ...item, ...updates } : item))
);
};
const deleteItem = (id: string) => {
const getChildIds = (parentId: string): string[] =>
items
.filter((item) => item.parentId === parentId)
.flatMap((child) => [child.id, ...getChildIds(child.id)]);
const idsToDelete = new Set([id, ...getChildIds(id)]);
input.onChange(items.filter((item) => !idsToDelete.has(item.id)));
};
const treeNodes = buildTree(items);
return (
<div className="w-full">
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white">
{/* Header */}
<div className="px-4 py-3 border-b border-gray-200 bg-slate-50 flex items-center justify-between">
<div className="flex gap-2">
<button
type="button"
onClick={() => addItem(null, "folder")}
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
>
<FolderIcon className="h-3 w-3" />
Add Folder
</button>
<button
type="button"
onClick={() => addItem(null, "file")}
className="flex items-center gap-1 px-2 py-1 text-xs bg-green-500 text-white rounded hover:bg-green-600"
>
<DocumentIcon className="h-3 w-3" />
Add File
</button>
</div>
</div>
{/* File Tree */}
<div className="p-4 min-h-[200px] font-mono text-sm">
{treeNodes.length === 0 ? (
<div className="text-center text-gray-500 py-8 max-w-full break-words text-wrap">
<div className="mb-2">No files or folders yet</div>
<div className="text-xs">
Click "Add Folder" or "Add File" to get started
</div>
</div>
) : (
treeNodes.map((node) => (
<div
key={`file-tree-item-${node.id}`}
className="w-full overflow-x-scroll"
>
<FileTreeItem
node={node}
editState={{ editingId, setEditingId }}
dragActions={{ dragState, setDragState }}
treeState={{ expandedFolders, toggleFolder }}
onUpdate={updateItem}
onDelete={deleteItem}
onMove={moveItem}
/>
</div>
))
)}
</div>
</div>
{/* Current items count */}
<div className="mt-2 text-xs text-gray-500">
{items.length} item{items.length !== 1 ? "s" : ""} total
</div>
</div>
);
});