Jaimodiji commited on
Commit
2798672
·
verified ·
1 Parent(s): a7de84b

Upload folder using huggingface_hub

Browse files
client/components/BackupControl.tsx CHANGED
@@ -1,8 +1,11 @@
1
- import { useState, useRef, ChangeEvent } from 'react'
2
- import { useEditor, TldrawUiMenuItem } from 'tldraw'
3
  import { useAuth } from '../hooks/useAuth'
4
  import { useBackups } from '../hooks/useBackups'
5
  import { exportToImage } from '../utils/exportUtils'
 
 
 
6
 
7
  export function BackupMenuItem({ roomId }: { roomId: string }) {
8
  const editor = useEditor()
@@ -62,7 +65,7 @@ export function BackupMenuItem({ roomId }: { roomId: string }) {
62
  export function DownloadMenuItem({ roomId }: { roomId: string }) {
63
  const editor = useEditor()
64
 
65
- const handleDownload = () => {
66
  try {
67
  const snapshot = editor.getSnapshot()
68
  const jsonStr = JSON.stringify({
@@ -72,15 +75,38 @@ export function DownloadMenuItem({ roomId }: { roomId: string }) {
72
  source: 'tldraw-multiplayer'
73
  }, null, 2)
74
 
75
- const blob = new Blob([jsonStr], { type: 'application/json' })
76
- const url = URL.createObjectURL(blob)
77
- const link = document.createElement('a')
78
- link.href = url
79
- link.download = `tldraw-room-${roomId}-${new Date().toISOString().slice(0,10)}.json`
80
- document.body.appendChild(link)
81
- link.click()
82
- document.body.removeChild(link)
83
- URL.revokeObjectURL(url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  } catch (e: any) {
85
  console.error('Failed to download backup', e)
86
  alert('Failed to download backup: ' + e.message)
@@ -99,12 +125,34 @@ export function DownloadMenuItem({ roomId }: { roomId: string }) {
99
  }
100
 
101
  export function RestoreMenuItem() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  const editor = useEditor()
103
  const inputRef = useRef<HTMLInputElement>(null)
104
 
105
- const handleRestoreClick = () => {
106
- inputRef.current?.click()
107
- }
 
 
 
 
 
 
108
 
109
  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
110
  const file = e.target.files?.[0]
@@ -119,37 +167,65 @@ export function RestoreMenuItem() {
119
  // Handle both raw snapshot or our wrapped format
120
  const snapshot = data.snapshot || data
121
 
122
- if (window.confirm('This will replace the current board content. Are you sure?')) {
123
- editor.loadSnapshot(snapshot)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
  } catch (e: any) {
126
  console.error('Failed to parse backup file', e)
127
- alert('Failed to restore backup: Invalid file format')
128
  } finally {
129
  // Reset input
130
  if (inputRef.current) inputRef.current.value = ''
131
  }
132
  }
 
 
 
133
  reader.readAsText(file)
134
  }
135
 
136
  return (
137
- <>
138
- <TldrawUiMenuItem
139
- id="restore-json"
140
- label="Restore from File"
141
- icon="external-link"
142
- readonlyOk
143
- onSelect={handleRestoreClick}
144
- />
145
- <input
146
- ref={inputRef}
147
- type="file"
148
- accept=".json"
149
- style={{ display: 'none' }}
150
- onChange={handleFileChange}
151
- />
152
- </>
153
  )
154
  }
155
 
 
1
+ import { useState, useRef, useEffect, ChangeEvent } from 'react'
2
+ import { useEditor, TldrawUiMenuItem, uniqueId } from 'tldraw'
3
  import { useAuth } from '../hooks/useAuth'
4
  import { useBackups } from '../hooks/useBackups'
5
  import { exportToImage } from '../utils/exportUtils'
6
+ import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'
7
+ import { Share } from '@capacitor/share'
8
+ import { Capacitor } from '@capacitor/core'
9
 
10
  export function BackupMenuItem({ roomId }: { roomId: string }) {
11
  const editor = useEditor()
 
65
  export function DownloadMenuItem({ roomId }: { roomId: string }) {
66
  const editor = useEditor()
67
 
68
+ const handleDownload = async () => {
69
  try {
70
  const snapshot = editor.getSnapshot()
71
  const jsonStr = JSON.stringify({
 
75
  source: 'tldraw-multiplayer'
76
  }, null, 2)
77
 
78
+ const fileName = `tldraw-room-${roomId}-${new Date().toISOString().slice(0,10)}.json`
79
+
80
+ if (Capacitor.isNativePlatform()) {
81
+ // Native (Android/iOS): Use Filesystem + Share
82
+ try {
83
+ const savedFile = await Filesystem.writeFile({
84
+ path: fileName,
85
+ data: jsonStr,
86
+ directory: Directory.Cache,
87
+ encoding: Encoding.UTF8
88
+ })
89
+
90
+ await Share.share({
91
+ title: 'Backup Board JSON',
92
+ files: [savedFile.uri],
93
+ })
94
+ } catch (err: any) {
95
+ console.error('Native save failed', err)
96
+ alert('Failed to save file: ' + err.message)
97
+ }
98
+ } else {
99
+ // Web: Use anchor tag download
100
+ const blob = new Blob([jsonStr], { type: 'application/json' })
101
+ const url = URL.createObjectURL(blob)
102
+ const link = document.createElement('a')
103
+ link.href = url
104
+ link.download = fileName
105
+ document.body.appendChild(link)
106
+ link.click()
107
+ document.body.removeChild(link)
108
+ URL.revokeObjectURL(url)
109
+ }
110
  } catch (e: any) {
111
  console.error('Failed to download backup', e)
112
  alert('Failed to download backup: ' + e.message)
 
125
  }
126
 
127
  export function RestoreMenuItem() {
128
+ const handleRestoreClick = () => {
129
+ window.dispatchEvent(new CustomEvent('tldraw-trigger-file-restore'))
130
+ }
131
+
132
+ return (
133
+ <TldrawUiMenuItem
134
+ id="restore-json"
135
+ label="Restore from File"
136
+ icon="external-link"
137
+ readonlyOk
138
+ onSelect={handleRestoreClick}
139
+ />
140
+ )
141
+ }
142
+
143
+ export function RestoreFileHandler({ roomId: _roomId }: { roomId: string }) {
144
  const editor = useEditor()
145
  const inputRef = useRef<HTMLInputElement>(null)
146
 
147
+ useEffect(() => {
148
+ const handleTrigger = () => {
149
+ if (inputRef.current) {
150
+ inputRef.current.click()
151
+ }
152
+ }
153
+ window.addEventListener('tldraw-trigger-file-restore', handleTrigger)
154
+ return () => window.removeEventListener('tldraw-trigger-file-restore', handleTrigger)
155
+ }, [])
156
 
157
  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
158
  const file = e.target.files?.[0]
 
167
  // Handle both raw snapshot or our wrapped format
168
  const snapshot = data.snapshot || data
169
 
170
+ const mode = window.confirm('Restore as a NEW board? (Click Cancel to OVERWRITE the current board)')
171
+
172
+ if (mode) {
173
+ // Restore as New
174
+ const newId = uniqueId()
175
+ const name = data.roomName || 'Restored Board'
176
+
177
+ // Store for the new room to pick up
178
+ localStorage.setItem(`restore_data_${newId}`, JSON.stringify({
179
+ snapshot,
180
+ roomName: name
181
+ }))
182
+
183
+ // Save to recents
184
+ const storedRooms = localStorage.getItem('tldraw_saved_rooms')
185
+ let recentRooms = []
186
+ try {
187
+ if (storedRooms) recentRooms = JSON.parse(storedRooms)
188
+ } catch (e) {}
189
+
190
+ recentRooms.push({
191
+ id: newId,
192
+ name: name,
193
+ lastVisited: Date.now()
194
+ })
195
+ localStorage.setItem('tldraw_saved_rooms', JSON.stringify(recentRooms))
196
+
197
+ // Open in new tab or navigate?
198
+ // Let's navigate to keep it simple, or open in new tab if requested.
199
+ // Implementation plan said "Creates new Room -> Loads snapshot -> Navigates to room"
200
+ window.location.assign(`/#/${newId}`)
201
+ } else {
202
+ // Overwrite Current
203
+ if (window.confirm('WARNING: This will permanently overwrite the current board content for everyone. Are you sure?')) {
204
+ editor.loadSnapshot(snapshot)
205
+ }
206
  }
207
  } catch (e: any) {
208
  console.error('Failed to parse backup file', e)
209
+ alert('Failed to restore backup: Invalid file format (' + e.message + ')')
210
  } finally {
211
  // Reset input
212
  if (inputRef.current) inputRef.current.value = ''
213
  }
214
  }
215
+ reader.onerror = (err) => {
216
+ alert('FileReader Error: ' + err)
217
+ }
218
  reader.readAsText(file)
219
  }
220
 
221
  return (
222
+ <input
223
+ ref={inputRef}
224
+ type="file"
225
+ accept="application/json,.json"
226
+ style={{ position: 'fixed', top: '-10000px', left: '-10000px', opacity: 0, pointerEvents: 'none' }}
227
+ onChange={handleFileChange}
228
+ />
 
 
 
 
 
 
 
 
 
229
  )
230
  }
231
 
client/components/Toast.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+
4
+ interface ToastProps {
5
+ message: string
6
+ duration?: number
7
+ type?: 'info' | 'success' | 'error'
8
+ onClose: () => void
9
+ }
10
+
11
+ function Toast({ message, duration = 3000, type = 'info', onClose }: ToastProps) {
12
+ const [visible, setVisible] = useState(false)
13
+
14
+ useEffect(() => {
15
+ // Trigger enter animation
16
+ requestAnimationFrame(() => setVisible(true))
17
+
18
+ const timer = setTimeout(() => {
19
+ setVisible(false)
20
+ // Wait for exit animation to finish before unmounting
21
+ setTimeout(onClose, 300)
22
+ }, duration)
23
+
24
+ return () => clearTimeout(timer)
25
+ }, [duration, onClose])
26
+
27
+ const getBgColor = () => {
28
+ switch (type) {
29
+ case 'success': return '#10b981' // Green
30
+ case 'error': return '#ef4444' // Red
31
+ default: return '#3b82f6' // Blue
32
+ }
33
+ }
34
+
35
+ return (
36
+ <div style={{
37
+ position: 'fixed',
38
+ bottom: 24,
39
+ left: '50%',
40
+ transform: `translateX(-50%) translateY(${visible ? 0 : 20}px)`,
41
+ opacity: visible ? 1 : 0,
42
+ background: getBgColor(),
43
+ color: '#fff',
44
+ padding: '10px 20px',
45
+ borderRadius: 8,
46
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
47
+ fontSize: '0.9rem',
48
+ fontWeight: 500,
49
+ zIndex: 10000,
50
+ transition: 'all 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
51
+ pointerEvents: 'none', // Allow clicks to pass through
52
+ display: 'flex',
53
+ alignItems: 'center',
54
+ gap: 8,
55
+ minWidth: 200,
56
+ justifyContent: 'center'
57
+ }}>
58
+ {type === 'success' && (
59
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
60
+ )}
61
+ {type === 'info' && (
62
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
63
+ )}
64
+ {message}
65
+ </div>
66
+ )
67
+ }
68
+
69
+ export function showToast(message: string, options: { duration?: number, type?: 'info' | 'success' | 'error' } = {}) {
70
+ const div = document.createElement('div')
71
+ document.body.appendChild(div)
72
+ const root = createRoot(div)
73
+
74
+ const cleanup = () => {
75
+ root.unmount()
76
+ if (document.body.contains(div)) {
77
+ document.body.removeChild(div)
78
+ }
79
+ }
80
+
81
+ root.render(
82
+ <Toast
83
+ message={message}
84
+ duration={options.duration}
85
+ type={options.type}
86
+ onClose={cleanup}
87
+ />
88
+ )
89
+ }
client/pages/RoomPage.tsx CHANGED
@@ -33,7 +33,7 @@ import { BrushManager } from '../components/BrushManager'
33
  import { EquationRenderer } from '../components/EquationRendererSimple'
34
  import { getEraserSettings } from '../utils/eraserUtils'
35
  import { EquationShapeUtil } from '../shapes/EquationShapeUtil'
36
- import { BackupMenuItem, DownloadMenuItem, RestoreMenuItem, PdfExportMenuItem } from '../components/BackupControl'
37
 
38
  const customShapeUtils = [
39
  ...defaultShapeUtils.filter(u => u.type !== 'geo'),
@@ -393,6 +393,7 @@ export function RoomPage() {
393
  }}
394
  >
395
  <SPenController />
 
396
  <StatePersistence roomId={roomId} />
397
  <NavigationDock roomId={roomId} />
398
  <SelectLockedLogic />
 
33
  import { EquationRenderer } from '../components/EquationRendererSimple'
34
  import { getEraserSettings } from '../utils/eraserUtils'
35
  import { EquationShapeUtil } from '../shapes/EquationShapeUtil'
36
+ import { BackupMenuItem, DownloadMenuItem, RestoreMenuItem, RestoreFileHandler, PdfExportMenuItem } from '../components/BackupControl'
37
 
38
  const customShapeUtils = [
39
  ...defaultShapeUtils.filter(u => u.type !== 'geo'),
 
393
  }}
394
  >
395
  <SPenController />
396
+ <RestoreFileHandler roomId={roomId} />
397
  <StatePersistence roomId={roomId} />
398
  <NavigationDock roomId={roomId} />
399
  <SelectLockedLogic />
client/utils/exportUtils.ts CHANGED
@@ -2,7 +2,7 @@ import { Share } from '@capacitor/share'
2
  import { Filesystem, Directory } from '@capacitor/filesystem'
3
  import { SERVER_URL } from '../config'
4
  import { Editor } from 'tldraw'
5
- import { customAlert, customPrompt } from './uiUtils'
6
  import { jsPDF } from 'jspdf'
7
  import 'svg2pdf.js'
8
 
@@ -48,6 +48,9 @@ export const exportToImage = async (editor: Editor, roomId: string, format: 'png
48
  return
49
  }
50
 
 
 
 
51
  // 2. Calculate Bounds to prevent OOM
52
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
53
  shapeIds.forEach(id => {
 
2
  import { Filesystem, Directory } from '@capacitor/filesystem'
3
  import { SERVER_URL } from '../config'
4
  import { Editor } from 'tldraw'
5
+ import { customAlert, customPrompt, showToast } from './uiUtils'
6
  import { jsPDF } from 'jspdf'
7
  import 'svg2pdf.js'
8
 
 
48
  return
49
  }
50
 
51
+ // Show quiet indicator
52
+ showToast(`Preparing ${format.toUpperCase()} export...`, { duration: 3000 })
53
+
54
  // 2. Calculate Bounds to prevent OOM
55
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
56
  shapeIds.forEach(id => {
client/utils/uiUtils.ts CHANGED
@@ -1,4 +1,7 @@
1
  import { showModal } from '../components/Modal'
 
 
 
2
 
3
  export const customAlert = async (message: string, title?: string) => {
4
  await showModal({
@@ -25,11 +28,8 @@ export const customPrompt = async (message: string, defaultValue?: string, title
25
  // but based on current Modal code, 'alert' type doesn't show Cancel button.
26
  // Let's UPDATE Modal.tsx to support 'confirm' type first.
27
  export const customConfirm = async (message: string, title?: string): Promise<boolean> => {
28
- // We need to update Modal.tsx to support this first.
29
- // For now, mapping to window.confirm as fallback or we can quickly update Modal.
30
- // Let's assume we will update Modal.tsx in the next step.
31
  const result = await showModal({
32
- type: 'confirm' as any, // We will add this type
33
  message,
34
  title,
35
  })
 
1
  import { showModal } from '../components/Modal'
2
+ import { showToast as showToastImpl } from '../components/Toast'
3
+
4
+ export const showToast = showToastImpl
5
 
6
  export const customAlert = async (message: string, title?: string) => {
7
  await showModal({
 
28
  // but based on current Modal code, 'alert' type doesn't show Cancel button.
29
  // Let's UPDATE Modal.tsx to support 'confirm' type first.
30
  export const customConfirm = async (message: string, title?: string): Promise<boolean> => {
 
 
 
31
  const result = await showModal({
32
+ type: 'confirm',
33
  message,
34
  title,
35
  })
deploy.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e # Exit on error
3
+
4
+ echo "🚀 Starting deployment process..."
5
+
6
+ # 1. Build the project
7
+ echo "📦 Building project..."
8
+ npm run build
9
+
10
+ # 2. Deploy to Cloudflare
11
+ echo "☁️ Deploying to Cloudflare..."
12
+ npx wrangler deploy
13
+
14
+ # 3. Upload to Hugging Face
15
+ echo "🤗 Uploading to Hugging Face..."
16
+ /home/u0_a369/.local/bin/hf upload Jaimodiji/my-multiplayer-app . . \
17
+ --repo-type space \
18
+ --exclude "node_modules/*" \
19
+ --exclude "dist/*" \
20
+ --exclude ".wrangler/*" \
21
+ --exclude ".git/*" \
22
+ --exclude ".env" \
23
+ --exclude ".dev.vars" \
24
+ --exclude "temp/*" \
25
+ --exclude "restored_files/*" \
26
+ --exclude "*.sqlite" \
27
+ --exclude "*.log"
28
+
29
+ echo "✅ Deployment completed successfully!"