Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- client/components/BackupControl.tsx +110 -34
- client/components/Toast.tsx +89 -0
- client/pages/RoomPage.tsx +2 -1
- client/utils/exportUtils.ts +4 -1
- client/utils/uiUtils.ts +4 -4
- deploy.sh +29 -0
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
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 106 |
-
|
| 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 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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'
|
| 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!"
|