Skip to content

Instantly share code, notes, and snippets.

@PatrickJS
Created September 20, 2024 04:16
Show Gist options
  • Save PatrickJS/9b97a3ffe00d5088a0ec2f8c81949c0b to your computer and use it in GitHub Desktop.
Save PatrickJS/9b97a3ffe00d5088a0ec2f8c81949c0b to your computer and use it in GitHub Desktop.
Basic Folder/File Recursive UI
import { component$, useStore, useSignal, useTask$ } from '@builder.io/qwik';
// Define the Node type
type Node = {
id: string;
name: string;
type: string;
tags: string[];
children?: Node[];
attributes?: Record<string, unknown>;
pending?: boolean;
};
// Define the PendingOperation type
type PendingOperation = {
type: 'create' | 'update' | 'delete';
node: Node;
path: string[];
};
// Sample data
const fileSystemData: Node = {
id: "root",
name: "Root",
type: "directory",
tags: ["root"],
children: [
{
id: "dir1",
name: "Work",
type: "directory",
tags: ["work"],
children: [
{
id: "dir1-1",
name: "Projects",
type: "directory",
tags: ["work", "projects"],
children: [
{
id: "dir1-1-1",
name: "Project A",
type: "directory",
tags: ["work", "project-a"],
children: [
{
id: "file1",
name: "Proposal.docx",
type: "file",
tags: ["document", "proposal"],
attributes: { size: 1024 }
},
{
id: "file2",
name: "Budget.xlsx",
type: "file",
tags: ["spreadsheet", "budget"],
attributes: { size: 2048 }
}
]
},
{
id: "dir1-1-2",
name: "Project B",
type: "directory",
tags: ["work", "project-b"],
children: [
{
id: "file3",
name: "Report.pdf",
type: "file",
tags: ["document", "report"],
attributes: { size: 3072 }
}
]
}
]
}
]
},
{
id: "dir2",
name: "Personal",
type: "directory",
tags: ["personal"],
children: [
{
id: "dir2-1",
name: "Photos",
type: "directory",
tags: ["personal", "photos"],
children: [
{
id: "file4",
name: "Vacation.jpg",
type: "file",
tags: ["image", "vacation"],
attributes: { size: 4096 }
}
]
},
{
id: "dir2-2",
name: "Documents",
type: "directory",
tags: ["personal", "documents"],
children: [
{
id: "file5",
name: "Resume.pdf",
type: "file",
tags: ["document", "resume"],
attributes: { size: 2560 }
}
]
}
]
}
]
};
export const App = component$(() => {
const state = useStore({
currentNode: fileSystemData,
path: [fileSystemData],
isCreating: null as 'file' | 'folder' | null,
newItemName: '',
newItemTags: '',
pendingOperations: [] as PendingOperation[]
});
const autoSaveSignal = useSignal(0);
useTask$(({ track }) => {
track(() => autoSaveSignal.value);
if (state.pendingOperations.length > 0) {
savePendingChanges();
}
});
const navigateTo = $((node: Node) => {
if (node.type === 'directory') {
state.currentNode = node;
const nodeIndex = state.path.findIndex(n => n.id === node.id);
if (nodeIndex !== -1) {
state.path = state.path.slice(0, nodeIndex + 1);
} else {
state.path = [...state.path, node];
}
}
});
const navigateUp = $(() => {
if (state.path.length > 1) {
state.path = state.path.slice(0, -1);
state.currentNode = state.path[state.path.length - 1];
}
});
const createNewItem = $(() => {
if (!state.newItemName) return;
const newItem: Node = {
id: Date.now().toString(),
name: state.newItemName,
type: state.isCreating === 'file' ? 'file' : 'directory',
tags: state.newItemTags.split(',').map(tag => tag.trim()),
...(state.isCreating === 'file' ? { attributes: { size: 0 } } : { children: [] }),
pending: true
};
const newOperation: PendingOperation = {
type: 'create',
node: newItem,
path: state.path.map(n => n.id)
};
state.pendingOperations = [...state.pendingOperations, newOperation];
state.currentNode.children = [...(state.currentNode.children || []), newItem];
state.isCreating = null;
state.newItemName = '';
state.newItemTags = '';
});
const savePendingChanges = $(async () => {
// Simulate an API call to save changes
await new Promise(resolve => setTimeout(resolve, 1000));
// Process each pending operation
state.pendingOperations.forEach(op => {
if (op.type === 'create') {
// In a real app, you'd update the server and get a new ID
op.node.pending = false;
}
// Handle other operation types as needed
});
// Clear pending operations
state.pendingOperations = [];
// Update the current node to reflect saved changes
if (state.currentNode.children) {
state.currentNode.children = state.currentNode.children.map(child => ({
...child,
pending: false
}));
}
});
// Set up auto-save every 5 seconds
setInterval(() => {
autoSaveSignal.value++;
}, 5000);
return (
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">File System Navigation</h1>
<div class="mb-4 flex items-center">
<button
onClick$={navigateUp}
class="bg-blue-500 text-white px-2 py-1 rounded mr-2 hover:bg-blue-600 transition-colors duration-200"
disabled={state.path.length === 1}
>
Up
</button>
{state.path.map((node, index) => (
<span key={node.id}>
<span
class="text-blue-500 hover:underline cursor-pointer"
onClick$={() => navigateTo(node)}
>
{node.name}
</span>
{index < state.path.length - 1 && <span class="mx-2">/</span>}
</span>
))}
</div>
{state.pendingOperations.length > 0 && (
<div class="mb-4 text-yellow-600">
{state.pendingOperations.length} unsaved changes.
<button
onClick$={savePendingChanges}
class="ml-2 bg-yellow-500 text-white px-2 py-1 rounded hover:bg-yellow-600"
>
Save Now
</button>
</div>
)}
<div class="mb-4">
<h2 class="text-xl font-semibold mb-2">Directories</h2>
<div class="grid grid-cols-4 gap-4">
{state.currentNode.children?.filter(node => node.type === 'directory').map(node => (
<div
key={node.id}
class={`border p-2 rounded cursor-pointer transition-colors duration-200 ${
node.pending ? 'bg-yellow-100' : 'bg-blue-100'
} hover:bg-blue-200`}
onClick$={() => navigateTo(node)}
>
<div class="font-bold">{node.name}</div>
<div class="text-sm text-gray-500">{node.type}</div>
<div class="text-xs text-gray-400">{node.tags.join(', ')}</div>
{node.pending && <div class="text-xs text-yellow-600">Pending</div>}
</div>
))}
<div
class="border-2 border-dashed border-gray-300 p-2 rounded cursor-pointer transition-colors duration-200 hover:bg-gray-100 flex items-center justify-center"
onClick$={() => state.isCreating = 'folder'}
>
<span class="text-gray-500">+ New Folder</span>
</div>
</div>
</div>
<div>
<h2 class="text-xl font-semibold mb-2">Files</h2>
<div class="grid grid-cols-4 gap-4">
{state.currentNode.children?.filter(node => node.type === 'file').map(node => (
<div
key={node.id}
class={`border p-2 rounded cursor-pointer transition-colors duration-200 ${
node.pending ? 'bg-yellow-100' : 'bg-white'
} hover:bg-gray-100`}
>
<div class="font-bold">{node.name}</div>
<div class="text-sm text-gray-500">{node.type}</div>
<div class="text-xs text-gray-400">{node.tags.join(', ')}</div>
{node.attributes?.size && (
<div class="text-xs text-gray-400">Size: {node.attributes.size} bytes</div>
)}
{node.pending && <div class="text-xs text-yellow-600">Pending</div>}
</div>
))}
<div
class="border-2 border-dashed border-gray-300 p-2 rounded cursor-pointer transition-colors duration-200 hover:bg-gray-100 flex items-center justify-center"
onClick$={() => state.isCreating = 'file'}
>
<span class="text-gray-500">+ New File</span>
</div>
</div>
</div>
{state.isCreating && (
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-white p-4 rounded">
<h3 class="font-bold mb-2">Create New {state.isCreating === 'file' ? 'File' : 'Folder'}</h3>
<input
type="text"
placeholder="Name"
value={state.newItemName}
onInput$={(e) => state.newItemName = (e.target as HTMLInputElement).value}
class="border p-1 mb-2 w-full"
/>
<input
type="text"
placeholder="Tags (comma-separated)"
value={state.newItemTags}
onInput$={(e) => state.newItemTags = (e.target as HTMLInputElement).value}
class="border p-1 mb-2 w-full"
/>
<div>
<button
onClick$={createNewItem}
class="bg-green-500 text-white px-2 py-1 rounded mr-2 hover:bg-green-600"
>
Create
</button>
<button
onClick$={() => state.isCreating = null}
class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
);
});
export default App;
import React, { useState, useEffect } from 'react';
// Define the Node type
type Node = {
id: string;
name: string;
type: string;
tags: string[];
children?: Node[];
attributes?: Record<string, unknown>;
pending?: boolean;
};
// Define the PendingOperation type
type PendingOperation = {
type: 'create' | 'update' | 'delete';
node: Node;
path: string[];
};
// Sample data (unchanged)
const fileSystemData: Node = {
// ... (previous sample data)
};
// Main App component
const App = () => {
const [currentNode, setCurrentNode] = useState<Node>(fileSystemData);
const [path, setPath] = useState<Node[]>([fileSystemData]);
const [isCreating, setIsCreating] = useState<'file' | 'folder' | null>(null);
const [newItemName, setNewItemName] = useState('');
const [newItemTags, setNewItemTags] = useState('');
const [pendingOperations, setPendingOperations] = useState<PendingOperation[]>([]);
const navigateTo = (node: Node) => {
if (node.type === 'directory') {
setCurrentNode(node);
const nodeIndex = path.findIndex(n => n.id === node.id);
if (nodeIndex !== -1) {
setPath(path.slice(0, nodeIndex + 1));
} else {
setPath([...path, node]);
}
}
};
const navigateUp = () => {
if (path.length > 1) {
const newPath = path.slice(0, -1);
setPath(newPath);
setCurrentNode(newPath[newPath.length - 1]);
}
};
const createNewItem = () => {
if (!newItemName) return;
const newItem: Node = {
id: Date.now().toString(),
name: newItemName,
type: isCreating === 'file' ? 'file' : 'directory',
tags: newItemTags.split(',').map(tag => tag.trim()),
...(isCreating === 'file' ? { attributes: { size: 0 } } : { children: [] }),
pending: true
};
const newOperation: PendingOperation = {
type: 'create',
node: newItem,
path: path.map(n => n.id)
};
setPendingOperations([...pendingOperations, newOperation]);
setCurrentNode(prevNode => ({
...prevNode,
children: [...(prevNode.children || []), newItem]
}));
setIsCreating(null);
setNewItemName('');
setNewItemTags('');
};
const savePendingChanges = async () => {
// Simulate an API call to save changes
await new Promise(resolve => setTimeout(resolve, 1000));
// Process each pending operation
pendingOperations.forEach(op => {
if (op.type === 'create') {
// In a real app, you'd update the server and get a new ID
op.node.pending = false;
}
// Handle other operation types as needed
});
// Clear pending operations
setPendingOperations([]);
// Update the current node to reflect saved changes
setCurrentNode(prevNode => ({
...prevNode,
children: prevNode.children?.map(child => ({
...child,
pending: false
}))
}));
};
useEffect(() => {
// Auto-save changes every 5 seconds if there are pending operations
const autoSaveInterval = setInterval(() => {
if (pendingOperations.length > 0) {
savePendingChanges();
}
}, 5000);
return () => clearInterval(autoSaveInterval);
}, [pendingOperations]);
const directories = currentNode.children?.filter(node => node.type === 'directory') || [];
const files = currentNode.children?.filter(node => node.type === 'file') || [];
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">File System Navigation</h1>
<div className="mb-4 flex items-center">
<button
onClick={navigateUp}
className="bg-blue-500 text-white px-2 py-1 rounded mr-2 hover:bg-blue-600 transition-colors duration-200"
disabled={path.length === 1}
>
Up
</button>
{path.map((node, index) => (
<span key={node.id}>
<span
className="text-blue-500 hover:underline cursor-pointer"
onClick={() => navigateTo(node)}
>
{node.name}
</span>
{index < path.length - 1 && <span className="mx-2">/</span>}
</span>
))}
</div>
{pendingOperations.length > 0 && (
<div className="mb-4 text-yellow-600">
{pendingOperations.length} unsaved changes.
<button
onClick={savePendingChanges}
className="ml-2 bg-yellow-500 text-white px-2 py-1 rounded hover:bg-yellow-600"
>
Save Now
</button>
</div>
)}
<div className="mb-4">
<h2 className="text-xl font-semibold mb-2">Directories</h2>
<div className="grid grid-cols-4 gap-4">
{directories.map(node => (
<div
key={node.id}
className={`border p-2 rounded cursor-pointer transition-colors duration-200 ${
node.pending ? 'bg-yellow-100' : 'bg-blue-100'
} hover:bg-blue-200`}
onClick={() => navigateTo(node)}
>
<div className="font-bold">{node.name}</div>
<div className="text-sm text-gray-500">{node.type}</div>
<div className="text-xs text-gray-400">{node.tags.join(', ')}</div>
{node.pending && <div className="text-xs text-yellow-600">Pending</div>}
</div>
))}
<div
className="border-2 border-dashed border-gray-300 p-2 rounded cursor-pointer transition-colors duration-200 hover:bg-gray-100 flex items-center justify-center"
onClick={() => setIsCreating('folder')}
>
<span className="text-gray-500">+ New Folder</span>
</div>
</div>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Files</h2>
<div className="grid grid-cols-4 gap-4">
{files.map(node => (
<div
key={node.id}
className={`border p-2 rounded cursor-pointer transition-colors duration-200 ${
node.pending ? 'bg-yellow-100' : 'bg-white'
} hover:bg-gray-100`}
>
<div className="font-bold">{node.name}</div>
<div className="text-sm text-gray-500">{node.type}</div>
<div className="text-xs text-gray-400">{node.tags.join(', ')}</div>
{node.attributes?.size && (
<div className="text-xs text-gray-400">Size: {node.attributes.size} bytes</div>
)}
{node.pending && <div className="text-xs text-yellow-600">Pending</div>}
</div>
))}
<div
className="border-2 border-dashed border-gray-300 p-2 rounded cursor-pointer transition-colors duration-200 hover:bg-gray-100 flex items-center justify-center"
onClick={() => setIsCreating('file')}
>
<span className="text-gray-500">+ New File</span>
</div>
</div>
</div>
{isCreating && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white p-4 rounded">
<h3 className="font-bold mb-2">Create New {isCreating === 'file' ? 'File' : 'Folder'}</h3>
<input
type="text"
placeholder="Name"
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
className="border p-1 mb-2 w-full"
/>
<input
type="text"
placeholder="Tags (comma-separated)"
value={newItemTags}
onChange={(e) => setNewItemTags(e.target.value)}
className="border p-1 mb-2 w-full"
/>
<div>
<button
onClick={createNewItem}
className="bg-green-500 text-white px-2 py-1 rounded mr-2 hover:bg-green-600"
>
Create
</button>
<button
onClick={() => setIsCreating(null)}
className="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment