Skip to content

Commit a743f41

Browse files
authored
Merge pull request #6 from CloudCannon/fix/add-collection-key
Added collection key and added collection calls to example
2 parents 216df3e + 18299ec commit a743f41

File tree

7 files changed

+285
-175
lines changed

7 files changed

+285
-175
lines changed

examples/file-browser/src/App.tsx

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useCloudCannonAPI } from './hooks/useCloudCannonAPI';
88
function App() {
99
const [selectedFile, setSelectedFile] = useState<CloudCannonJavaScriptV1APIFile | null>(null);
1010

11-
const { api, isLoading, error, files, refreshFiles } = useCloudCannonAPI();
11+
const { api, isLoading, error, files, refreshFiles, collections } = useCloudCannonAPI();
1212

1313
const handleFileSelect = (file: CloudCannonJavaScriptV1APIFile) => {
1414
setSelectedFile(file);
@@ -69,27 +69,12 @@ function App() {
6969

7070
return (
7171
<div className="h-screen flex flex-col bg-gray-100">
72-
{/* Header */}
73-
<header className="bg-white border-b border-gray-200 px-4 py-3">
74-
<div className="flex items-center justify-between">
75-
<h1 className="text-xl font-semibold text-gray-800">CloudCannon File Browser</h1>
76-
<div className="flex items-center space-x-2 text-sm text-gray-500">
77-
<span>{files.length} files</span>
78-
{selectedFile && (
79-
<>
80-
<span></span>
81-
<span>Editing: {selectedFile.path}</span>
82-
</>
83-
)}
84-
</div>
85-
</div>
86-
</header>
87-
8872
{/* Main content */}
8973
<div className="flex-1 flex overflow-hidden">
9074
{/* File browser sidebar */}
9175
<div className="w-80 flex-shrink-0">
9276
<FileBrowser
77+
collections={collections}
9378
files={files}
9479
selectedFile={selectedFile}
9580
onFileSelect={handleFileSelect}
@@ -108,6 +93,7 @@ function App() {
10893
{/* Status bar */}
10994
<footer className="bg-blue-600 text-white px-4 py-2 text-sm">
11095
<div className="flex items-center justify-between">
96+
<div className="flex items-center space-x-4" />
11197
<div className="flex items-center space-x-4">
11298
<span>CloudCannon API v1</span>
11399
{api && (
@@ -116,8 +102,6 @@ function App() {
116102
Connected
117103
</span>
118104
)}
119-
</div>
120-
<div className="flex items-center space-x-4">
121105
{selectedFile && (
122106
<span>Language: {selectedFile.path.split('.').pop()?.toUpperCase() || 'PLAIN'}</span>
123107
)}

examples/file-browser/src/components/CodeEditor.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export function CodeEditor({ file, onSave }: CodeEditorProps) {
9292
};
9393

9494
loadFileContent();
95+
96+
file.addEventListener('change', async () => {
97+
const fileContent = await file.get();
98+
setOriginalContent(fileContent);
99+
});
95100
}, [file]);
96101

97102
useEffect(() => {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type {
2+
CloudCannonJavaScriptV1APICollection,
3+
CloudCannonJavaScriptV1APIFile,
4+
} from '@cloudcannon/javascript-api';
5+
import { RefreshCw } from 'lucide-react';
6+
import { useEffect, useState } from 'react';
7+
import { FileTree } from './FileTree';
8+
9+
interface CollectionBrowserProps {
10+
collection: CloudCannonJavaScriptV1APICollection;
11+
selectedFile: CloudCannonJavaScriptV1APIFile | null;
12+
onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void;
13+
isLoading: boolean;
14+
onRefresh: () => void;
15+
}
16+
17+
export function CollectionBrowser({
18+
collection,
19+
selectedFile,
20+
onFileSelect,
21+
isLoading,
22+
onRefresh,
23+
}: CollectionBrowserProps) {
24+
const [items, setItems] = useState<CloudCannonJavaScriptV1APIFile[] | undefined>(undefined);
25+
26+
useEffect(() => {
27+
const handleFileChange = async () => {
28+
const items = await collection.items();
29+
setItems(items);
30+
};
31+
32+
collection.addEventListener('change', handleFileChange);
33+
collection.addEventListener('create', handleFileChange);
34+
collection.addEventListener('delete', handleFileChange);
35+
handleFileChange();
36+
37+
return () => {
38+
collection.removeEventListener('change', handleFileChange);
39+
collection.removeEventListener('create', handleFileChange);
40+
collection.removeEventListener('delete', handleFileChange);
41+
};
42+
}, [collection]);
43+
44+
return (
45+
<div className="flex flex-col bg-white border-r border-gray-200">
46+
{/* Header */}
47+
<div className="flex items-center justify-between p-3 border-b border-t border-gray-200 bg-gray-50">
48+
<h2 className="font-medium text-gray-700">{collection.collectionKey}</h2>
49+
<button
50+
type="button"
51+
onClick={onRefresh}
52+
disabled={isLoading}
53+
className="p-1 text-gray-500 hover:text-gray-700 disabled:opacity-50"
54+
title="Refresh collection"
55+
>
56+
<RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
57+
</button>
58+
</div>
59+
60+
<FileTree
61+
files={items || []}
62+
selectedFile={selectedFile}
63+
onFileSelect={onFileSelect}
64+
isLoading={isLoading || !items}
65+
/>
66+
</div>
67+
);
68+
}
Lines changed: 27 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,30 @@
1-
import type { CloudCannonJavaScriptV1APIFile } from '@cloudcannon/javascript-api';
2-
import { AlertCircle, ChevronDown, ChevronRight, File, Folder, RefreshCw } from 'lucide-react';
3-
import { useMemo, useState } from 'react';
1+
import type {
2+
CloudCannonJavaScriptV1APICollection,
3+
CloudCannonJavaScriptV1APIFile,
4+
} from '@cloudcannon/javascript-api';
5+
import { AlertCircle, RefreshCw } from 'lucide-react';
6+
import { CollectionBrowser } from './CollectionBrowser';
7+
import { FileTree } from './FileTree';
48

59
interface FileBrowserProps {
610
files: CloudCannonJavaScriptV1APIFile[];
11+
collections: CloudCannonJavaScriptV1APICollection[];
712
selectedFile: CloudCannonJavaScriptV1APIFile | null;
813
onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void;
914
isLoading: boolean;
1015
error: string | null;
1116
onRefresh: () => void;
1217
}
1318

14-
interface FileTreeNode {
15-
name: string;
16-
path: string;
17-
type: 'file' | 'directory';
18-
children?: Record<string, FileTreeNode>;
19-
file?: CloudCannonJavaScriptV1APIFile;
20-
}
21-
22-
function buildFileTree(files: CloudCannonJavaScriptV1APIFile[]): FileTreeNode[] {
23-
const root: Record<string, FileTreeNode> = {};
24-
25-
files.forEach((file) => {
26-
const parts = file.path.split('/').filter(Boolean);
27-
let current = root;
28-
29-
parts.forEach((part, index) => {
30-
const isFile = index === parts.length - 1;
31-
const currentPath = parts.slice(0, index + 1).join('/');
32-
33-
if (!current[part]) {
34-
current[part] = {
35-
name: part,
36-
path: currentPath,
37-
type: isFile ? 'file' : 'directory',
38-
children: isFile ? undefined : {},
39-
file: isFile ? file : undefined,
40-
};
41-
}
42-
43-
if (!isFile) {
44-
current = current[part].children as Record<string, FileTreeNode>;
45-
}
46-
});
47-
});
48-
49-
// Convert to array and sort
50-
const sortNodes = (nodes: Record<string, FileTreeNode>): FileTreeNode[] => {
51-
return Object.values(nodes)
52-
.map((node) => ({
53-
...node,
54-
children: node.children ? sortNodes(node.children) : {},
55-
}))
56-
.sort((a, b) => {
57-
// Directories first, then files
58-
if (a.type !== b.type) {
59-
return a.type === 'directory' ? -1 : 1;
60-
}
61-
return a.name.localeCompare(b.name);
62-
});
63-
};
64-
65-
return sortNodes(root);
66-
}
67-
68-
interface FileTreeItemProps {
69-
node: FileTreeNode;
70-
selectedFile: CloudCannonJavaScriptV1APIFile | null;
71-
onFileSelect: (file: CloudCannonJavaScriptV1APIFile) => void;
72-
level: number;
73-
}
74-
75-
function FileTreeItem({ node, selectedFile, onFileSelect, level }: FileTreeItemProps) {
76-
const [isExpanded, setIsExpanded] = useState(level < 2); // Auto-expand first two levels
77-
const isSelected = selectedFile?.path === node.path;
78-
79-
const handleClick = () => {
80-
if (node.type === 'directory') {
81-
setIsExpanded(!isExpanded);
82-
} else if (node.file) {
83-
onFileSelect(node.file);
84-
}
85-
};
86-
87-
const getFileIcon = (_filename: string) => {
88-
// const ext = filename.split('.').pop()?.toLowerCase();
89-
// You could expand this with more specific file type icons
90-
return <File size={16} />;
91-
};
92-
93-
return (
94-
<div>
95-
<div
96-
className={`
97-
flex items-center px-2 py-1 cursor-pointer hover:bg-gray-100
98-
${isSelected ? 'bg-blue-100 border-r-2 border-blue-500' : ''}
99-
`}
100-
style={{ paddingLeft: `${level * 16 + 8}px` }}
101-
onClick={handleClick}
102-
onKeyDown={(e) => {
103-
if (e.key === 'Enter' || e.key === ' ') {
104-
handleClick();
105-
}
106-
}}
107-
>
108-
{node.type === 'directory' && (
109-
<span className="mr-1 text-gray-400">
110-
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
111-
</span>
112-
)}
113-
114-
<span className="mr-2 text-gray-600">
115-
{node.type === 'directory' ? <Folder size={16} /> : getFileIcon(node.name)}
116-
</span>
117-
118-
<span className={`text-sm ${isSelected ? 'font-medium text-blue-700' : 'text-gray-700'}`}>
119-
{node.name}
120-
</span>
121-
</div>
122-
123-
{node.type === 'directory' && isExpanded && node.children && (
124-
<div>
125-
{Object.values(node.children).map((child) => (
126-
<FileTreeItem
127-
key={child.path}
128-
node={child}
129-
selectedFile={selectedFile}
130-
onFileSelect={onFileSelect}
131-
level={level + 1}
132-
/>
133-
))}
134-
</div>
135-
)}
136-
</div>
137-
);
138-
}
139-
14019
export function FileBrowser({
14120
files,
21+
collections,
14222
selectedFile,
14323
onFileSelect,
14424
isLoading,
14525
error,
14626
onRefresh,
14727
}: FileBrowserProps) {
148-
const fileTree = useMemo(() => buildFileTree(files), [files]);
149-
15028
if (error) {
15129
return (
15230
<div className="p-4">
@@ -168,8 +46,19 @@ export function FileBrowser({
16846

16947
return (
17048
<div className="h-full flex flex-col bg-white border-r border-gray-200">
49+
{collections.map((collection) => (
50+
<CollectionBrowser
51+
key={collection.collectionKey}
52+
collection={collection}
53+
selectedFile={selectedFile}
54+
onFileSelect={onFileSelect}
55+
isLoading={isLoading}
56+
onRefresh={onRefresh}
57+
/>
58+
))}
59+
17160
{/* Header */}
172-
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gray-50">
61+
<div className="flex items-center justify-between p-3 border-b border-t border-gray-200 bg-gray-50">
17362
<h2 className="font-medium text-gray-700">Files</h2>
17463
<button
17564
type="button"
@@ -183,30 +72,12 @@ export function FileBrowser({
18372
</div>
18473

18574
{/* File tree */}
186-
<div className="flex-1 overflow-auto">
187-
{isLoading ? (
188-
<div className="p-4 text-center text-gray-500">
189-
<RefreshCw size={20} className="animate-spin mx-auto mb-2" />
190-
<p className="text-sm">Loading files...</p>
191-
</div>
192-
) : fileTree.length === 0 ? (
193-
<div className="p-4 text-center text-gray-500">
194-
<p className="text-sm">No files found</p>
195-
</div>
196-
) : (
197-
<div className="py-2">
198-
{fileTree.map((node) => (
199-
<FileTreeItem
200-
key={node.path}
201-
node={node}
202-
selectedFile={selectedFile}
203-
onFileSelect={onFileSelect}
204-
level={0}
205-
/>
206-
))}
207-
</div>
208-
)}
209-
</div>
75+
<FileTree
76+
files={files}
77+
selectedFile={selectedFile}
78+
onFileSelect={onFileSelect}
79+
isLoading={isLoading}
80+
/>
21081
</div>
21182
);
21283
}

0 commit comments

Comments
 (0)