mohsin-devs commited on
Commit
61fcdc2
·
1 Parent(s): b1df25f

Fix page refresh on navigation and folder clicks - use button elements and prevent default

Browse files
Files changed (9) hide show
  1. .dockerignore +0 -16
  2. Dockerfile +0 -28
  3. README.md +3 -1
  4. app.js +87 -37
  5. index.html +23 -23
  6. js/api/hfService.js +41 -40
  7. js/main.js +7 -3
  8. js/ui/uiRenderer.js +2 -0
  9. server/app.py +1 -0
.dockerignore DELETED
@@ -1,16 +0,0 @@
1
- .git
2
- .gitignore
3
- .DS_Store
4
- __pycache__
5
- *.pyc
6
- .pytest_cache
7
- .env
8
- venv/
9
- env/
10
- logs/
11
- data/
12
- *.log
13
- .vscode
14
- .idea
15
- node_modules
16
- .env.local
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,28 +0,0 @@
1
- # Use official Python runtime as a parent image
2
- FROM python:3.10-slim
3
-
4
- # Set working directory in container
5
- WORKDIR /app
6
-
7
- # Copy requirements first for better caching
8
- COPY server/requirements.txt .
9
-
10
- # Install Python dependencies
11
- RUN pip install --no-cache-dir -r requirements.txt
12
-
13
- # Copy the entire project
14
- COPY . .
15
-
16
- # Create data and logs directories
17
- RUN mkdir -p data logs
18
-
19
- # Set environment variables
20
- ENV FLASK_APP=server/app.py
21
- ENV FLASK_ENV=production
22
- ENV PYTHONUNBUFFERED=1
23
-
24
- # Expose port 5000 for Flask
25
- EXPOSE 5000
26
-
27
- # Run the Flask application
28
- CMD ["python", "-m", "flask", "run", "--host", "0.0.0.0"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,7 +3,9 @@ title: DocVault App
3
  emoji: 📁
4
  colorFrom: blue
5
  colorTo: purple
6
- sdk: docker
 
 
7
  ---
8
 
9
  # DocVault - Offline-First Document Storage System
 
3
  emoji: 📁
4
  colorFrom: blue
5
  colorTo: purple
6
+ sdk: static
7
+ app_file: index.html
8
+ pinned: false
9
  ---
10
 
11
  # DocVault - Offline-First Document Storage System
app.js CHANGED
@@ -1,19 +1,7 @@
1
  // DocVault — Offline-First Document Storage System
2
  // Uses local Flask backend for all operations
3
 
4
- // Determine API base URL based on environment
5
- const API_BASE = (() => {
6
- const host = window.location.hostname;
7
- const isLocal = ['localhost', '127.0.0.1'].includes(host);
8
-
9
- if (isLocal) {
10
- return 'http://localhost:5000/api';
11
- } else {
12
- // For HF Spaces and other deployments, use root path
13
- return '/api';
14
- }
15
- })();
16
-
17
  const USER_ID = 'default_user';
18
  const DEFAULT_FOLDER = '';
19
 
@@ -36,17 +24,26 @@ async function apiFetch(endpoint, options = {}) {
36
  async function listFilesAPI(path = DEFAULT_FOLDER) {
37
  try {
38
  const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
 
 
39
  const res = await apiFetch(`/list${queryPath}`);
40
 
 
 
41
  if (!res.ok) {
42
- if (res.status === 404) return { files: [], folders: [] };
43
- throw new Error(`Failed to list files: ${res.status}`);
 
 
 
44
  }
45
 
46
  const data = await res.json();
 
 
47
  if (!data.success) {
48
  console.error('API error:', data.error);
49
- return { files: [], folders: [] };
50
  }
51
 
52
  const files = (data.files || []).map(f => ({
@@ -66,9 +63,11 @@ async function listFilesAPI(path = DEFAULT_FOLDER) {
66
  modified_at: f.modified_at
67
  }));
68
 
 
69
  return { files, folders };
70
  } catch (err) {
71
  console.error('List files error:', err);
 
72
  return { files: [], folders: [] };
73
  }
74
  }
@@ -79,23 +78,32 @@ async function uploadFileAPI(file, destPath) {
79
  const fileBlob = file instanceof File ? file : file.content;
80
  const filename = file instanceof File ? file.name : (file.name || 'upload.bin');
81
 
 
 
82
  const formData = new FormData();
83
  formData.append('folder_path', folderPath);
84
  formData.append('file', fileBlob, filename);
85
 
86
- const res = await fetch(`${API_BASE}/upload-file`, {
 
 
 
87
  method: 'POST',
88
  headers: { 'X-User-ID': USER_ID },
89
  body: formData
90
  });
91
 
 
 
92
  if (!res.ok) {
93
- const errData = await res.json();
94
- throw new Error(errData.error || `Upload failed: ${res.status}`);
95
  }
96
 
97
  const data = await res.json();
98
- if (!data.success) throw new Error(data.error);
 
 
99
 
100
  return data;
101
  } catch (err) {
@@ -362,6 +370,7 @@ function showSkeletons(container, count = 6) {
362
  // ─── FETCH FILES ──────────────────────────────────────────
363
  async function fetchAndRender() {
364
  if (isFetching) {
 
365
  return;
366
  }
367
  isFetching = true;
@@ -372,11 +381,16 @@ async function fetchAndRender() {
372
  showSkeletons(filesContainer, 6);
373
  try {
374
  const prefix = getFolderPath();
 
375
  const pathSnapshot = JSON.stringify(currentPath); // Capture for safety check
376
  const { files, folders } = await listFilesAPI(prefix);
377
 
 
 
378
  // SAFETY CHECK: Path may have changed due to user clicking elsewhere
379
  if (JSON.stringify(currentPath) !== pathSnapshot) {
 
 
380
  return;
381
  }
382
 
@@ -504,26 +518,23 @@ function renderFolders(folders) {
504
  </div>
505
  </div>`;
506
 
507
- // NAVIGATION HANDLER - Use a self-contained function to avoid closure issues
508
- const handleFolderClick = (e) => {
 
 
 
509
  // Don't navigate if clicking on the actions menu
510
  if (e.target.closest('.folder-actions')) {
511
  return;
512
  }
513
 
514
- // Stop all propagation for actual navigation clicks
515
- e.preventDefault();
516
- e.stopPropagation();
517
- e.stopImmediatePropagation();
518
-
519
  // Navigate into the folder
520
- currentPath.push(name);
521
- addToRecent(folder.path, name, 'folder');
 
 
522
  fetchAndRender();
523
- return false;
524
- };
525
-
526
- card.addEventListener('click', handleFolderClick, true); // Use capture phase
527
 
528
  // Attach menu functionality
529
  attachCardMenu(card, folder.path, 'folder');
@@ -860,7 +871,10 @@ function hideProgress() { uploadProgress.classList.remove('active'); }
860
 
861
  // ─── FILE INPUT ───────────────────────────────────────────
862
  fileInput.addEventListener('change', () => {
863
- uploadFiles(fileInput.files);
 
 
 
864
  fileInput.value = '';
865
  });
866
 
@@ -877,9 +891,24 @@ createFolderBtn.addEventListener('click', () => {
877
  createFolderModal.classList.add('active');
878
  setTimeout(() => folderNameInput.focus(), 100);
879
  });
880
- uploadFileBtn.addEventListener('click', () => {
 
 
 
881
  newDropdown.classList.remove('active');
882
- fileInput.click();
 
 
 
 
 
 
 
 
 
 
 
 
883
  });
884
 
885
  // ─── CREATE FOLDER ────────────────────────────────────────
@@ -921,26 +950,37 @@ function setNavActive(nav) {
921
  }
922
 
923
  navMyFiles.addEventListener('click', (e) => {
 
924
  e.preventDefault();
925
  e.stopPropagation();
926
  e.stopImmediatePropagation();
927
- currentBrowse = 'files'; currentPath = [];
 
928
  setNavActive(navMyFiles);
929
  fetchAndRender();
 
930
  });
931
 
932
  navRecent.addEventListener('click', (e) => {
 
933
  e.preventDefault();
 
 
934
  currentBrowse = 'recent';
935
  setNavActive(navRecent);
936
  renderRecentView();
 
937
  });
938
 
939
  navStarred.addEventListener('click', (e) => {
 
940
  e.preventDefault();
 
 
941
  currentBrowse = 'starred';
942
  setNavActive(navStarred);
943
  renderStarredView();
 
944
  });
945
 
946
  function renderRecentView() {
@@ -1080,6 +1120,16 @@ confirmRenameBtn.addEventListener('click', async () => {
1080
  // ─── INIT ─────────────────────────────────────────────────
1081
  // Initialize offline-first DocVault
1082
  (function initApp() {
 
 
 
 
 
 
 
 
 
 
1083
  // Show welcome message
1084
  showToast('🎉 Welcome to DocVault! Loading your files...', 'info');
1085
 
 
1
  // DocVault — Offline-First Document Storage System
2
  // Uses local Flask backend for all operations
3
 
4
+ const API_BASE = 'http://localhost:5000/api';
 
 
 
 
 
 
 
 
 
 
 
 
5
  const USER_ID = 'default_user';
6
  const DEFAULT_FOLDER = '';
7
 
 
24
  async function listFilesAPI(path = DEFAULT_FOLDER) {
25
  try {
26
  const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
27
+ const url = `${API_BASE}/list${queryPath}`;
28
+ console.log('Fetching from URL:', url); // Debug
29
  const res = await apiFetch(`/list${queryPath}`);
30
 
31
+ console.log('API Response status:', res.status); // Debug
32
+
33
  if (!res.ok) {
34
+ if (res.status === 404) {
35
+ console.warn('Path not found, returning empty list'); // Debug
36
+ return { files: [], folders: [] };
37
+ }
38
+ throw new Error(`Failed to list files: ${res.status} ${res.statusText}`);
39
  }
40
 
41
  const data = await res.json();
42
+ console.log('API Data:', data); // Debug
43
+
44
  if (!data.success) {
45
  console.error('API error:', data.error);
46
+ throw new Error(data.error || 'API returned success: false');
47
  }
48
 
49
  const files = (data.files || []).map(f => ({
 
63
  modified_at: f.modified_at
64
  }));
65
 
66
+ console.log('Parsed folders:', folders.length, 'files:', files.length); // Debug
67
  return { files, folders };
68
  } catch (err) {
69
  console.error('List files error:', err);
70
+ showToast(`Error loading files: ${err.message}`, 'error');
71
  return { files: [], folders: [] };
72
  }
73
  }
 
78
  const fileBlob = file instanceof File ? file : file.content;
79
  const filename = file instanceof File ? file.name : (file.name || 'upload.bin');
80
 
81
+ console.log('Uploading file:', filename, 'to:', folderPath); // Debug
82
+
83
  const formData = new FormData();
84
  formData.append('folder_path', folderPath);
85
  formData.append('file', fileBlob, filename);
86
 
87
+ const url = `${API_BASE}/upload-file`;
88
+ console.log('Upload endpoint:', url); // Debug
89
+
90
+ const res = await fetch(url, {
91
  method: 'POST',
92
  headers: { 'X-User-ID': USER_ID },
93
  body: formData
94
  });
95
 
96
+ console.log('Upload response status:', res.status); // Debug
97
+
98
  if (!res.ok) {
99
+ const errData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
100
+ throw new Error(errData.error || `Upload failed: ${res.status} ${res.statusText}`);
101
  }
102
 
103
  const data = await res.json();
104
+ console.log('Upload API response:', data); // Debug
105
+
106
+ if (!data.success) throw new Error(data.error || 'Upload API returned success: false');
107
 
108
  return data;
109
  } catch (err) {
 
370
  // ─── FETCH FILES ──────────────────────────────────────────
371
  async function fetchAndRender() {
372
  if (isFetching) {
373
+ console.warn('Already fetching, ignoring request');
374
  return;
375
  }
376
  isFetching = true;
 
381
  showSkeletons(filesContainer, 6);
382
  try {
383
  const prefix = getFolderPath();
384
+ console.log('Fetching contents for path:', prefix); // Debug
385
  const pathSnapshot = JSON.stringify(currentPath); // Capture for safety check
386
  const { files, folders } = await listFilesAPI(prefix);
387
 
388
+ console.log('API returned:', { folders: folders.length, files: files.length }); // Debug
389
+
390
  // SAFETY CHECK: Path may have changed due to user clicking elsewhere
391
  if (JSON.stringify(currentPath) !== pathSnapshot) {
392
+ console.log('Path changed during fetch'); // Debug
393
+ isFetching = false;
394
  return;
395
  }
396
 
 
518
  </div>
519
  </div>`;
520
 
521
+ // FIXED: Simplified folder click handler
522
+ card.addEventListener('click', function(e) {
523
+ e.preventDefault();
524
+ e.stopPropagation();
525
+
526
  // Don't navigate if clicking on the actions menu
527
  if (e.target.closest('.folder-actions')) {
528
  return;
529
  }
530
 
 
 
 
 
 
531
  // Navigate into the folder
532
+ const folderName = folder.path.split('/').pop();
533
+ console.log('Navigating to folder:', folderName, 'Full path:', folder.path); // Debug
534
+ currentPath.push(folderName);
535
+ addToRecent(folder.path, folderName, 'folder');
536
  fetchAndRender();
537
+ });
 
 
 
538
 
539
  // Attach menu functionality
540
  attachCardMenu(card, folder.path, 'folder');
 
871
 
872
  // ─── FILE INPUT ───────────────────────────────────────────
873
  fileInput.addEventListener('change', () => {
874
+ console.log('File input changed, files:', fileInput.files.length); // Debug
875
+ if (fileInput.files && fileInput.files.length > 0) {
876
+ uploadFiles(fileInput.files);
877
+ }
878
  fileInput.value = '';
879
  });
880
 
 
891
  createFolderModal.classList.add('active');
892
  setTimeout(() => folderNameInput.focus(), 100);
893
  });
894
+ uploadFileBtn.addEventListener('click', (e) => {
895
+ e.preventDefault();
896
+ e.stopPropagation();
897
+ console.log('Upload button clicked'); // Debug
898
  newDropdown.classList.remove('active');
899
+ // Use try-catch in case fileInput doesn't exist
900
+ try {
901
+ if (fileInput) {
902
+ fileInput.click();
903
+ console.log('fileInput.click() called'); // Debug
904
+ } else {
905
+ console.error('fileInput element not found');
906
+ showToast('Error: File input not available', 'error');
907
+ }
908
+ } catch (err) {
909
+ console.error('Error clicking file input:', err);
910
+ showToast('Error: Could not open file dialog', 'error');
911
+ }
912
  });
913
 
914
  // ─── CREATE FOLDER ────────────────────────────────────────
 
950
  }
951
 
952
  navMyFiles.addEventListener('click', (e) => {
953
+ console.log('navMyFiles clicked'); // Debug
954
  e.preventDefault();
955
  e.stopPropagation();
956
  e.stopImmediatePropagation();
957
+ currentBrowse = 'files';
958
+ currentPath = [];
959
  setNavActive(navMyFiles);
960
  fetchAndRender();
961
+ return false;
962
  });
963
 
964
  navRecent.addEventListener('click', (e) => {
965
+ console.log('navRecent clicked'); // Debug
966
  e.preventDefault();
967
+ e.stopPropagation();
968
+ e.stopImmediatePropagation();
969
  currentBrowse = 'recent';
970
  setNavActive(navRecent);
971
  renderRecentView();
972
+ return false;
973
  });
974
 
975
  navStarred.addEventListener('click', (e) => {
976
+ console.log('navStarred clicked'); // Debug
977
  e.preventDefault();
978
+ e.stopPropagation();
979
+ e.stopImmediatePropagation();
980
  currentBrowse = 'starred';
981
  setNavActive(navStarred);
982
  renderStarredView();
983
+ return false;
984
  });
985
 
986
  function renderRecentView() {
 
1120
  // ─── INIT ─────────────────────────────────────────────────
1121
  // Initialize offline-first DocVault
1122
  (function initApp() {
1123
+ console.log('🚀 DocVault initializing...'); // Debug
1124
+ console.log('API_BASE:', API_BASE); // Debug
1125
+ console.log('USER_ID:', USER_ID); // Debug
1126
+ console.log('DOM Elements check:', {
1127
+ fileInput: !!fileInput,
1128
+ uploadFileBtn: !!uploadFileBtn,
1129
+ foldersContainer: !!foldersContainer,
1130
+ filesContainer: !!filesContainer
1131
+ }); // Debug
1132
+
1133
  // Show welcome message
1134
  showToast('🎉 Welcome to DocVault! Loading your files...', 'info');
1135
 
index.html CHANGED
@@ -38,29 +38,29 @@
38
  </div>
39
 
40
  <div class="new-btn-wrapper">
41
- <button class="btn-primary new-btn" id="newBtn">
42
  <i class="ph-bold ph-plus"></i> New
43
  </button>
44
  <div class="new-dropdown" id="newDropdown">
45
- <button class="new-dropdown-item" id="createFolderBtn">
46
  <i class="ph-fill ph-folder-plus"></i> Create Folder
47
  </button>
48
- <button class="new-dropdown-item" id="uploadFileBtn">
49
  <i class="ph-fill ph-upload-simple"></i> Upload File
50
  </button>
51
  </div>
52
  </div>
53
 
54
  <nav class="sidebar-nav">
55
- <a href="#" class="nav-item active" id="navMyFiles">
56
  <i class="ph-fill ph-folder"></i> My Files
57
- </a>
58
- <a href="#" class="nav-item" id="navRecent">
59
  <i class="ph-fill ph-clock-counter-clockwise"></i> Recent
60
- </a>
61
- <a href="#" class="nav-item" id="navStarred">
62
  <i class="ph-fill ph-star"></i> Starred
63
- </a>
64
  </nav>
65
 
66
  <div class="sidebar-bottom">
@@ -102,8 +102,8 @@
102
  <div class="section-header">
103
  <h2>Folders</h2>
104
  <div class="view-toggles">
105
- <button class="icon-btn active" id="viewGrid" title="Grid"><i class="ph-fill ph-squares-four"></i></button>
106
- <button class="icon-btn" id="viewList" title="List"><i class="ph-fill ph-list-dashes"></i></button>
107
  </div>
108
  </div>
109
  <div class="grid-container" id="foldersContainer"></div>
@@ -121,14 +121,14 @@
121
  <!-- Create Folder -->
122
  <div class="modal-overlay" id="createFolderModal">
123
  <div class="modal glass-panel">
124
- <button class="close-modal" id="closeNameModal"><i class="ph-bold ph-x"></i></button>
125
  <h3><i class="ph-fill ph-folder-plus" style="color:var(--folder-color);margin-right:10px"></i>New Folder</h3>
126
  <div class="input-group">
127
  <input type="text" id="folderNameInput" placeholder="Enter folder name..." autocomplete="off">
128
  </div>
129
  <div class="modal-footer">
130
- <button class="btn-secondary" id="cancelFolderBtn">Cancel</button>
131
- <button class="btn-primary" id="confirmFolderBtn">Create</button>
132
  </div>
133
  </div>
134
  </div>
@@ -136,14 +136,14 @@
136
  <!-- Delete Confirmation -->
137
  <div class="modal-overlay" id="deleteModal">
138
  <div class="modal glass-panel" style="max-width:360px">
139
- <button class="close-modal" id="closeDeleteModal"><i class="ph-bold ph-x"></i></button>
140
  <div class="delete-icon-wrap"><i class="ph-fill ph-warning"></i></div>
141
  <p style="text-align:center; margin-bottom:20px; font-size:15px; color:var(--text-main);">
142
  Are you sure you want to delete <strong>this item</strong>?
143
  </p>
144
  <div class="modal-footer" style="justify-content:center;gap:16px">
145
- <button class="btn-secondary" id="cancelDeleteBtn">Cancel</button>
146
- <button class="btn-danger" id="confirmDeleteBtn"><i class="ph-fill ph-trash"></i> Delete</button>
147
  </div>
148
  </div>
149
  </div>
@@ -151,15 +151,15 @@
151
  <!-- Rename Modal -->
152
  <div class="modal-overlay" id="renameModal">
153
  <div class="modal glass-panel" style="max-width:400px">
154
- <button class="close-modal" id="closeRenameModal"><i class="ph-bold ph-x"></i></button>
155
  <h3><i class="ph-fill ph-pencil-simple" style="color:var(--primary-color)"></i> Rename File</h3>
156
  <div class="input-group">
157
  <label class="input-label">New Name</label>
158
  <input type="text" id="renameInput" placeholder="Enter new name..." autocomplete="off">
159
  </div>
160
  <div class="modal-footer">
161
- <button class="btn-secondary" id="cancelRenameBtn">Cancel</button>
162
- <button class="btn-primary" id="confirmRenameBtn"><i class="ph-fill ph-check"></i> Rename</button>
163
  </div>
164
  </div>
165
  </div>
@@ -175,8 +175,8 @@
175
  <span id="previewFileName">filename.pdf</span>
176
  </div>
177
  <div style="display:flex; gap:12px">
178
- <button class="icon-btn" id="downloadFromPreview" title="Download"><i class="ph-bold ph-download-simple"></i></button>
179
- <button class="close-modal" id="closePreviewModal" style="position:static"><i class="ph-bold ph-x"></i></button>
180
  </div>
181
  </div>
182
  <div class="preview-body" id="previewBody">
@@ -185,6 +185,6 @@
185
  </div>
186
  </div>
187
 
188
- <script src="app.js"></script>
189
  </body>
190
  </html>
 
38
  </div>
39
 
40
  <div class="new-btn-wrapper">
41
+ <button type="button" class="btn-primary new-btn" id="newBtn">
42
  <i class="ph-bold ph-plus"></i> New
43
  </button>
44
  <div class="new-dropdown" id="newDropdown">
45
+ <button type="button" class="new-dropdown-item" id="createFolderBtn">
46
  <i class="ph-fill ph-folder-plus"></i> Create Folder
47
  </button>
48
+ <button type="button" class="new-dropdown-item" id="uploadFileBtn">
49
  <i class="ph-fill ph-upload-simple"></i> Upload File
50
  </button>
51
  </div>
52
  </div>
53
 
54
  <nav class="sidebar-nav">
55
+ <button type="button" class="nav-item active" id="navMyFiles">
56
  <i class="ph-fill ph-folder"></i> My Files
57
+ </button>
58
+ <button type="button" class="nav-item" id="navRecent">
59
  <i class="ph-fill ph-clock-counter-clockwise"></i> Recent
60
+ </button>
61
+ <button type="button" class="nav-item" id="navStarred">
62
  <i class="ph-fill ph-star"></i> Starred
63
+ </button>
64
  </nav>
65
 
66
  <div class="sidebar-bottom">
 
102
  <div class="section-header">
103
  <h2>Folders</h2>
104
  <div class="view-toggles">
105
+ <button type="button" class="icon-btn active" id="viewGrid" title="Grid"><i class="ph-fill ph-squares-four"></i></button>
106
+ <button type="button" class="icon-btn" id="viewList" title="List"><i class="ph-fill ph-list-dashes"></i></button>
107
  </div>
108
  </div>
109
  <div class="grid-container" id="foldersContainer"></div>
 
121
  <!-- Create Folder -->
122
  <div class="modal-overlay" id="createFolderModal">
123
  <div class="modal glass-panel">
124
+ <button type="button" class="close-modal" id="closeNameModal"><i class="ph-bold ph-x"></i></button>
125
  <h3><i class="ph-fill ph-folder-plus" style="color:var(--folder-color);margin-right:10px"></i>New Folder</h3>
126
  <div class="input-group">
127
  <input type="text" id="folderNameInput" placeholder="Enter folder name..." autocomplete="off">
128
  </div>
129
  <div class="modal-footer">
130
+ <button type="button" class="btn-secondary" id="cancelFolderBtn">Cancel</button>
131
+ <button type="button" class="btn-primary" id="confirmFolderBtn">Create</button>
132
  </div>
133
  </div>
134
  </div>
 
136
  <!-- Delete Confirmation -->
137
  <div class="modal-overlay" id="deleteModal">
138
  <div class="modal glass-panel" style="max-width:360px">
139
+ <button type="button" class="close-modal" id="closeDeleteModal"><i class="ph-bold ph-x"></i></button>
140
  <div class="delete-icon-wrap"><i class="ph-fill ph-warning"></i></div>
141
  <p style="text-align:center; margin-bottom:20px; font-size:15px; color:var(--text-main);">
142
  Are you sure you want to delete <strong>this item</strong>?
143
  </p>
144
  <div class="modal-footer" style="justify-content:center;gap:16px">
145
+ <button type="button" class="btn-secondary" id="cancelDeleteBtn">Cancel</button>
146
+ <button type="button" class="btn-danger" id="confirmDeleteBtn"><i class="ph-fill ph-trash"></i> Delete</button>
147
  </div>
148
  </div>
149
  </div>
 
151
  <!-- Rename Modal -->
152
  <div class="modal-overlay" id="renameModal">
153
  <div class="modal glass-panel" style="max-width:400px">
154
+ <button type="button" class="close-modal" id="closeRenameModal"><i class="ph-bold ph-x"></i></button>
155
  <h3><i class="ph-fill ph-pencil-simple" style="color:var(--primary-color)"></i> Rename File</h3>
156
  <div class="input-group">
157
  <label class="input-label">New Name</label>
158
  <input type="text" id="renameInput" placeholder="Enter new name..." autocomplete="off">
159
  </div>
160
  <div class="modal-footer">
161
+ <button type="button" class="btn-secondary" id="cancelRenameBtn">Cancel</button>
162
+ <button type="button" class="btn-primary" id="confirmRenameBtn"><i class="ph-fill ph-check"></i> Rename</button>
163
  </div>
164
  </div>
165
  </div>
 
175
  <span id="previewFileName">filename.pdf</span>
176
  </div>
177
  <div style="display:flex; gap:12px">
178
+ <button type="button" class="icon-btn" id="downloadFromPreview" title="Download"><i class="ph-bold ph-download-simple"></i></button>
179
+ <button type="button" class="close-modal" id="closePreviewModal" style="position:static"><i class="ph-bold ph-x"></i></button>
180
  </div>
181
  </div>
182
  <div class="preview-body" id="previewBody">
 
185
  </div>
186
  </div>
187
 
188
+ <script type="module" src="js/main.js?v=2"></script>
189
  </body>
190
  </html>
js/api/hfService.js CHANGED
@@ -37,34 +37,42 @@ class HFService {
37
  }
38
  }
39
 
40
- async listFiles(path = '', recursive = false) {
41
- const cacheKey = `list-${path}-${recursive}`;
42
  const cached = this.cache.get(cacheKey);
43
  if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
44
  return cached.data;
45
  }
46
 
47
- const url = `${this.apiBase}/list?path=${encodeURIComponent(path)}&recursive=${recursive}`;
48
- const res = await this.fetchWithRetry(url);
 
 
 
49
  const data = await res.json();
50
 
51
  const result = { files: [], folders: [] };
52
 
53
- if (Array.isArray(data)) {
54
- for (const item of data) {
55
- if (item.type === 'file' && !item.path.endsWith('/.gitkeep') && item.path !== '.gitkeep') {
56
- result.files.push({
57
- path: item.path,
58
- name: item.path.split('/').pop(),
59
- size: item.size || 0,
60
- type: 'file',
61
- lastModified: item.lastModified
62
- });
63
- } else if (item.type === 'directory') {
 
 
 
 
 
64
  result.folders.push({
65
  path: item.path,
66
- name: item.path.split('/').pop(),
67
- type: 'directory'
68
  });
69
  }
70
  }
@@ -75,17 +83,20 @@ class HFService {
75
  }
76
 
77
  async uploadFile(file, destPath) {
78
- const base64Content = await this.fileToBase64(file);
79
- const url = `${this.apiBase}/upload`;
 
 
 
 
 
 
 
80
 
81
  const res = await this.fetchWithRetry(url, {
82
  method: 'POST',
83
- headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify({
85
- path: destPath,
86
- content: base64Content,
87
- summary: `Upload ${destPath.split('/').pop()}`
88
- }),
89
  });
90
 
91
  this.clearCache();
@@ -93,11 +104,11 @@ class HFService {
93
  }
94
 
95
  async deleteFile(path) {
96
- const url = `${this.apiBase}/delete`;
97
  await this.fetchWithRetry(url, {
98
  method: 'POST',
99
- headers: { 'Content-Type': 'application/json' },
100
- body: JSON.stringify({ path }),
101
  });
102
 
103
  this.clearCache();
@@ -108,24 +119,14 @@ class HFService {
108
  const url = `${this.apiBase}/delete-folder`;
109
  const res = await this.fetchWithRetry(url, {
110
  method: 'POST',
111
- headers: { 'Content-Type': 'application/json' },
112
- body: JSON.stringify({ path: folderPath }),
113
  });
114
 
115
  this.clearCache();
116
  return await res.json();
117
  }
118
 
119
- async fileToBase64(file) {
120
- return new Promise((resolve, reject) => {
121
- const reader = new FileReader();
122
- const blob = file instanceof File ? file : file.content;
123
- reader.readAsDataURL(blob);
124
- reader.onload = () => resolve(reader.result.split(',')[1]);
125
- reader.onerror = reject;
126
- });
127
- }
128
-
129
  clearCache() {
130
  this.cache.clear();
131
  }
 
37
  }
38
  }
39
 
40
+ async listFiles(path = '') {
41
+ const cacheKey = `list-${path}`;
42
  const cached = this.cache.get(cacheKey);
43
  if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
44
  return cached.data;
45
  }
46
 
47
+ const queryPath = path ? `?folder_path=${encodeURIComponent(path)}` : '';
48
+ const url = `${this.apiBase}/list${queryPath}`;
49
+
50
+ // Add X-User-ID header to match app.py expected requests
51
+ const res = await this.fetchWithRetry(url, { headers: { 'X-User-ID': 'default_user' } });
52
  const data = await res.json();
53
 
54
  const result = { files: [], folders: [] };
55
 
56
+ if (data && data.success) {
57
+ if (data.files) {
58
+ for (const item of data.files) {
59
+ if (!item.path.endsWith('/.gitkeep') && item.path !== '.gitkeep') {
60
+ result.files.push({
61
+ path: item.path,
62
+ name: item.name,
63
+ size: item.size || 0,
64
+ type: 'file',
65
+ lastModified: item.modified_at
66
+ });
67
+ }
68
+ }
69
+ }
70
+ if (data.folders) {
71
+ for (const item of data.folders) {
72
  result.folders.push({
73
  path: item.path,
74
+ name: item.name,
75
+ type: 'folder'
76
  });
77
  }
78
  }
 
83
  }
84
 
85
  async uploadFile(file, destPath) {
86
+ const formData = new FormData();
87
+ const folderPath = destPath.includes('/') ? destPath.substring(0, destPath.lastIndexOf('/')) : '';
88
+ const filename = file instanceof File ? file.name : destPath.split('/').pop();
89
+ const fileBlob = file instanceof File ? file : new Blob([file.content || '']);
90
+
91
+ formData.append('folder_path', folderPath);
92
+ formData.append('file', fileBlob, filename);
93
+
94
+ const url = `${this.apiBase}/upload-file`;
95
 
96
  const res = await this.fetchWithRetry(url, {
97
  method: 'POST',
98
+ headers: { 'X-User-ID': 'default_user' }, // Let browser set Content-Type with boundary
99
+ body: formData
 
 
 
 
100
  });
101
 
102
  this.clearCache();
 
104
  }
105
 
106
  async deleteFile(path) {
107
+ const url = `${this.apiBase}/delete-file`;
108
  await this.fetchWithRetry(url, {
109
  method: 'POST',
110
+ headers: { 'Content-Type': 'application/json', 'X-User-ID': 'default_user' },
111
+ body: JSON.stringify({ file_path: path }),
112
  });
113
 
114
  this.clearCache();
 
119
  const url = `${this.apiBase}/delete-folder`;
120
  const res = await this.fetchWithRetry(url, {
121
  method: 'POST',
122
+ headers: { 'Content-Type': 'application/json', 'X-User-ID': 'default_user' },
123
+ body: JSON.stringify({ folder_path: folderPath, force: true }),
124
  });
125
 
126
  this.clearCache();
127
  return await res.json();
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
130
  clearCache() {
131
  this.cache.clear();
132
  }
js/main.js CHANGED
@@ -1,4 +1,4 @@
1
- import { hfService } from './api/hfService.js';
2
  import { stateManager } from './state/stateManager.js';
3
  import { UIRenderer } from './ui/uiRenderer.js';
4
  import { getFileUrl, isImage, isPDF, isText } from './utils/formatters.js';
@@ -97,11 +97,15 @@ class App {
97
  e.stopPropagation();
98
  document.getElementById('newDropdown').classList.toggle('active');
99
  };
100
- document.getElementById('uploadFileBtn').onclick = () => {
 
 
101
  document.getElementById('newDropdown').classList.remove('active');
102
  document.getElementById('fileInput').click();
103
  };
104
- document.getElementById('createFolderBtn').onclick = () => {
 
 
105
  document.getElementById('newDropdown').classList.remove('active');
106
  document.getElementById('createFolderModal').classList.add('active');
107
  document.getElementById('folderNameInput').value = '';
 
1
+ import { hfService } from './api/hfService.js?v=2';
2
  import { stateManager } from './state/stateManager.js';
3
  import { UIRenderer } from './ui/uiRenderer.js';
4
  import { getFileUrl, isImage, isPDF, isText } from './utils/formatters.js';
 
97
  e.stopPropagation();
98
  document.getElementById('newDropdown').classList.toggle('active');
99
  };
100
+ document.getElementById('uploadFileBtn').onclick = (e) => {
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
  document.getElementById('newDropdown').classList.remove('active');
104
  document.getElementById('fileInput').click();
105
  };
106
+ document.getElementById('createFolderBtn').onclick = (e) => {
107
+ e.preventDefault();
108
+ e.stopPropagation();
109
  document.getElementById('newDropdown').classList.remove('active');
110
  document.getElementById('createFolderModal').classList.add('active');
111
  document.getElementById('folderNameInput').value = '';
js/ui/uiRenderer.js CHANGED
@@ -91,6 +91,8 @@ export class UIRenderer {
91
  <div class="item-meta">Folder</div>`;
92
 
93
  card.onclick = (e) => {
 
 
94
  if (e.target.closest('.card-menu')) return;
95
  onFolderClick(folder.name);
96
  };
 
91
  <div class="item-meta">Folder</div>`;
92
 
93
  card.onclick = (e) => {
94
+ e.preventDefault();
95
+ e.stopPropagation();
96
  if (e.target.closest('.card-menu')) return;
97
  onFolderClick(folder.name);
98
  };
server/app.py CHANGED
@@ -39,6 +39,7 @@ def create_app():
39
  def add_cache_headers(response):
40
  if response.content_type and ('text/html' in response.content_type or
41
  'text/javascript' in response.content_type or
 
42
  'text/css' in response.content_type):
43
  response.cache_control.max_age = 0
44
  response.cache_control.no_cache = True
 
39
  def add_cache_headers(response):
40
  if response.content_type and ('text/html' in response.content_type or
41
  'text/javascript' in response.content_type or
42
+ 'application/javascript' in response.content_type or
43
  'text/css' in response.content_type):
44
  response.cache_control.max_age = 0
45
  response.cache_control.no_cache = True