Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
87
fork

Configure Feed

Select the types of activity you want to include in your feed.

why did i let claude write so much react i hate fixing it myself

+253 -212
+69 -55
apps/main-app/public/editor/editor.tsx
··· 16 16 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@public/components/ui/tabs' 17 17 import Layout from '@public/layouts' 18 18 import { Loader2, LogOut, Trash2 } from 'lucide-react' 19 - import { type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent, useEffect, useState } from 'react' 19 + import { type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useState } from 'react' 20 20 import { createRoot } from 'react-dom/client' 21 21 import { BrowserRouter, Route, Routes } from 'react-router-dom' 22 22 import { useDomainData } from './hooks/useDomainData' ··· 133 133 }, [activeTab, configuringSite]) 134 134 135 135 // Handle site configuration modal 136 - const handleConfigureSite = async (site: SiteWithDomains) => { 136 + const handleConfigureSite = useCallback(async (site: SiteWithDomains) => { 137 137 setConfiguringSite(site) 138 138 139 139 // Build set of currently mapped domains ··· 207 207 setCorsEnabled(false) 208 208 setCorsOrigin('*') 209 209 } 210 - } 210 + }, []) 211 211 212 212 const handleSaveSiteConfig = async () => { 213 213 if (!configuringSite) return ··· 306 306 } 307 307 } 308 308 309 - const handleDeleteConfirmSite = async (site: SiteWithDomains) => { 309 + const handleDeleteConfirmSite = useCallback((site: SiteWithDomains) => { 310 310 setDeleteConfirmSite(site) 311 - } 311 + }, []) 312 312 313 313 const handleDeleteSite = async () => { 314 314 const site = configuringSite || deleteConfirmSite ··· 325 325 setIsDeletingSite(false) 326 326 } 327 327 328 - const handleUploadComplete = async () => { 328 + const handleUploadComplete = useCallback(async () => { 329 329 await fetchSites() 330 - } 330 + }, [fetchSites]) 331 331 332 332 const handleLogout = async () => { 333 333 try { ··· 470 470 </TabsTrigger> 471 471 </TabsList> 472 472 473 - {/* Sites Tab */} 474 - <TabsContent value="sites" className="flex-1 m-0 mt-4 overflow-hidden data-[state=inactive]:hidden"> 475 - <SitesTab 476 - sites={sites} 477 - sitesLoading={sitesLoading} 478 - userInfo={userInfo} 479 - onConfigureSite={handleConfigureSite} 480 - onDeleteSite={handleDeleteConfirmSite} 481 - /> 482 - </TabsContent> 473 + {/* Tab content — all tabs stay in the layout tree via absolute positioning to avoid 474 + the display:none→visible reflow cost. Inactive tabs use visibility:hidden instead. */} 475 + <div className="flex-1 relative mt-4 overflow-hidden"> 476 + <TabsContent 477 + value="sites" 478 + className="absolute inset-0 overflow-hidden data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none" 479 + > 480 + <SitesTab 481 + sites={sites} 482 + sitesLoading={sitesLoading} 483 + userInfo={userInfo} 484 + onConfigureSite={handleConfigureSite} 485 + onDeleteSite={handleDeleteConfirmSite} 486 + /> 487 + </TabsContent> 483 488 484 - {/* Domains Tab */} 485 - <TabsContent value="domains" className="flex-1 m-0 mt-4 overflow-hidden data-[state=inactive]:hidden"> 486 - <DomainsTab 487 - wispDomains={wispDomains} 488 - customDomains={customDomains} 489 - domainsLoading={domainsLoading} 490 - verificationStatus={verificationStatus} 491 - userInfo={userInfo} 492 - onAddCustomDomain={addCustomDomain} 493 - onVerifyDomain={verifyDomain} 494 - onDeleteCustomDomain={deleteCustomDomain} 495 - onDeleteWispDomain={deleteWispDomain} 496 - onClaimWispDomain={claimWispDomain} 497 - onCheckWispAvailability={checkWispAvailability} 498 - /> 499 - </TabsContent> 489 + <TabsContent 490 + value="domains" 491 + className="absolute inset-0 overflow-hidden data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none" 492 + > 493 + <DomainsTab 494 + wispDomains={wispDomains} 495 + customDomains={customDomains} 496 + domainsLoading={domainsLoading} 497 + verificationStatus={verificationStatus} 498 + userInfo={userInfo} 499 + onAddCustomDomain={addCustomDomain} 500 + onVerifyDomain={verifyDomain} 501 + onDeleteCustomDomain={deleteCustomDomain} 502 + onDeleteWispDomain={deleteWispDomain} 503 + onClaimWispDomain={claimWispDomain} 504 + onCheckWispAvailability={checkWispAvailability} 505 + /> 506 + </TabsContent> 500 507 501 - {/* Upload Tab */} 502 - <TabsContent value="upload" className="flex-1 m-0 mt-4 overflow-hidden data-[state=inactive]:hidden"> 503 - <UploadTab sites={sites} sitesLoading={sitesLoading} onUploadComplete={handleUploadComplete} /> 504 - </TabsContent> 508 + <TabsContent 509 + value="upload" 510 + className="absolute inset-0 overflow-hidden data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none" 511 + > 512 + <UploadTab sites={sites} sitesLoading={sitesLoading} onUploadComplete={handleUploadComplete} /> 513 + </TabsContent> 505 514 506 - {/* Webhooks Tab */} 507 - <TabsContent value="webhooks" className="flex-1 m-0 mt-4 overflow-hidden data-[state=inactive]:hidden"> 508 - <WebhooksTab 509 - webhooks={webhooks} 510 - webhooksLoading={webhooksLoading} 511 - eventLogs={eventLogs} 512 - eventLogsLoading={eventLogsLoading} 513 - isCreating={isCreating} 514 - userDid={userInfo?.did} 515 - onCreateWebhook={createWebhook} 516 - onDeleteWebhook={deleteWebhook} 517 - onRefreshEvents={fetchEventLogs} 518 - /> 519 - </TabsContent> 515 + <TabsContent 516 + value="webhooks" 517 + className="absolute inset-0 overflow-hidden data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none" 518 + > 519 + <WebhooksTab 520 + webhooks={webhooks} 521 + webhooksLoading={webhooksLoading} 522 + eventLogs={eventLogs} 523 + eventLogsLoading={eventLogsLoading} 524 + isCreating={isCreating} 525 + userDid={userInfo?.did} 526 + onCreateWebhook={createWebhook} 527 + onDeleteWebhook={deleteWebhook} 528 + onRefreshEvents={fetchEventLogs} 529 + /> 530 + </TabsContent> 520 531 521 - {/* CLI Tab */} 522 - <TabsContent value="cli" className="flex-1 m-0 mt-4 overflow-hidden data-[state=inactive]:hidden"> 523 - <CLITab /> 524 - </TabsContent> 532 + <TabsContent 533 + value="cli" 534 + className="absolute inset-0 overflow-hidden data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none" 535 + > 536 + <CLITab /> 537 + </TabsContent> 538 + </div> 525 539 </Tabs> 526 540 </div> 527 541 </div>
+135 -118
apps/main-app/public/editor/hooks/useDomainData.ts
··· 1 - import { useState } from 'react' 1 + import { useCallback, useState } from 'react' 2 2 3 3 export interface CustomDomain { 4 4 id: string ··· 25 25 [id: string]: VerificationStatus 26 26 }>({}) 27 27 28 - const fetchDomains = async () => { 28 + const fetchDomains = useCallback(async () => { 29 29 try { 30 30 const response = await fetch('/api/user/domains') 31 31 const data = await response.json() ··· 36 36 } finally { 37 37 setDomainsLoading(false) 38 38 } 39 - } 39 + }, []) 40 40 41 - const addCustomDomain = async (domain: string) => { 42 - try { 43 - const response = await fetch('/api/domain/custom/add', { 44 - method: 'POST', 45 - headers: { 'Content-Type': 'application/json' }, 46 - body: JSON.stringify({ domain }), 47 - }) 41 + const addCustomDomain = useCallback( 42 + async (domain: string) => { 43 + try { 44 + const response = await fetch('/api/domain/custom/add', { 45 + method: 'POST', 46 + headers: { 'Content-Type': 'application/json' }, 47 + body: JSON.stringify({ domain }), 48 + }) 48 49 49 - const data = await response.json() 50 - if (data.success) { 51 - await fetchDomains() 52 - return { success: true, id: data.id } 53 - } else { 54 - throw new Error(data.error || 'Failed to add domain') 50 + const data = await response.json() 51 + if (data.success) { 52 + await fetchDomains() 53 + return { success: true, id: data.id } 54 + } else { 55 + throw new Error(data.error || 'Failed to add domain') 56 + } 57 + } catch (err) { 58 + console.error('Add domain error:', err) 59 + alert(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 60 + return { success: false } 55 61 } 56 - } catch (err) { 57 - console.error('Add domain error:', err) 58 - alert(`Failed to add domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 59 - return { success: false } 60 - } 61 - } 62 + }, 63 + [fetchDomains], 64 + ) 62 65 63 - const verifyDomain = async (id: string): Promise<{ warning?: string }> => { 64 - setVerificationStatus({ ...verificationStatus, [id]: 'verifying' }) 66 + const verifyDomain = useCallback( 67 + async (id: string): Promise<{ warning?: string }> => { 68 + setVerificationStatus((prev) => ({ ...prev, [id]: 'verifying' })) 65 69 66 - try { 67 - const response = await fetch('/api/domain/custom/verify', { 68 - method: 'POST', 69 - headers: { 'Content-Type': 'application/json' }, 70 - body: JSON.stringify({ id }), 71 - }) 70 + try { 71 + const response = await fetch('/api/domain/custom/verify', { 72 + method: 'POST', 73 + headers: { 'Content-Type': 'application/json' }, 74 + body: JSON.stringify({ id }), 75 + }) 72 76 73 - const data = await response.json() 74 - if (data.success && data.verified) { 75 - setVerificationStatus({ ...verificationStatus, [id]: 'success' }) 76 - await fetchDomains() 77 - return { warning: data.warning } 78 - } else { 79 - setVerificationStatus({ ...verificationStatus, [id]: 'error' }) 80 - if (data.error) { 81 - alert(`Verification failed: ${data.error}`) 77 + const data = await response.json() 78 + if (data.success && data.verified) { 79 + setVerificationStatus((prev) => ({ ...prev, [id]: 'success' })) 80 + await fetchDomains() 81 + return { warning: data.warning } 82 + } else { 83 + setVerificationStatus((prev) => ({ ...prev, [id]: 'error' })) 84 + if (data.error) { 85 + alert(`Verification failed: ${data.error}`) 86 + } 87 + return {} 82 88 } 89 + } catch (err) { 90 + console.error('Verify domain error:', err) 91 + setVerificationStatus((prev) => ({ ...prev, [id]: 'error' })) 92 + alert(`Verification failed: ${err instanceof Error ? err.message : 'Unknown error'}`) 83 93 return {} 84 94 } 85 - } catch (err) { 86 - console.error('Verify domain error:', err) 87 - setVerificationStatus({ ...verificationStatus, [id]: 'error' }) 88 - alert(`Verification failed: ${err instanceof Error ? err.message : 'Unknown error'}`) 89 - return {} 90 - } 91 - } 95 + }, 96 + [fetchDomains], 97 + ) 92 98 93 - const deleteCustomDomain = async (id: string) => { 94 - if (!confirm('Are you sure you want to remove this custom domain?')) { 95 - return false 96 - } 99 + const deleteCustomDomain = useCallback( 100 + async (id: string) => { 101 + if (!confirm('Are you sure you want to remove this custom domain?')) { 102 + return false 103 + } 97 104 98 - try { 99 - const response = await fetch(`/api/domain/custom/${id}`, { 100 - method: 'DELETE', 101 - }) 105 + try { 106 + const response = await fetch(`/api/domain/custom/${id}`, { 107 + method: 'DELETE', 108 + }) 102 109 103 - const data = await response.json() 104 - if (data.success) { 105 - await fetchDomains() 106 - return true 107 - } else { 108 - throw new Error('Failed to delete domain') 110 + const data = await response.json() 111 + if (data.success) { 112 + await fetchDomains() 113 + return true 114 + } else { 115 + throw new Error('Failed to delete domain') 116 + } 117 + } catch (err) { 118 + console.error('Delete domain error:', err) 119 + alert(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 120 + return false 109 121 } 110 - } catch (err) { 111 - console.error('Delete domain error:', err) 112 - alert(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 113 - return false 114 - } 115 - } 122 + }, 123 + [fetchDomains], 124 + ) 116 125 117 - const mapWispDomain = async (domain: string, siteRkey: string | null) => { 126 + const mapWispDomain = useCallback(async (domain: string, siteRkey: string | null) => { 118 127 try { 119 128 const response = await fetch('/api/domain/wisp/map-site', { 120 129 method: 'POST', ··· 128 137 console.error('Map wisp domain error:', err) 129 138 throw err 130 139 } 131 - } 140 + }, []) 132 141 133 - const deleteWispDomain = async (domain: string) => { 134 - if (!confirm('Are you sure you want to remove this wisp.place domain?')) { 135 - return false 136 - } 142 + const deleteWispDomain = useCallback( 143 + async (domain: string) => { 144 + if (!confirm('Are you sure you want to remove this wisp.place domain?')) { 145 + return false 146 + } 137 147 138 - try { 139 - const response = await fetch(`/api/domain/wisp/${encodeURIComponent(domain)}`, { 140 - method: 'DELETE', 141 - }) 148 + try { 149 + const response = await fetch(`/api/domain/wisp/${encodeURIComponent(domain)}`, { 150 + method: 'DELETE', 151 + }) 142 152 143 - const data = await response.json() 144 - if (data.success) { 145 - await fetchDomains() 146 - return true 147 - } else { 148 - throw new Error('Failed to delete domain') 153 + const data = await response.json() 154 + if (data.success) { 155 + await fetchDomains() 156 + return true 157 + } else { 158 + throw new Error('Failed to delete domain') 159 + } 160 + } catch (err) { 161 + console.error('Delete wisp domain error:', err) 162 + alert(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 163 + return false 149 164 } 150 - } catch (err) { 151 - console.error('Delete wisp domain error:', err) 152 - alert(`Failed to delete domain: ${err instanceof Error ? err.message : 'Unknown error'}`) 153 - return false 154 - } 155 - } 165 + }, 166 + [fetchDomains], 167 + ) 156 168 157 - const mapCustomDomain = async (domainId: string, siteRkey: string | null) => { 169 + const mapCustomDomain = useCallback(async (domainId: string, siteRkey: string | null) => { 158 170 try { 159 171 const response = await fetch(`/api/domain/custom/${domainId}/map-site`, { 160 172 method: 'POST', ··· 168 180 console.error('Map custom domain error:', err) 169 181 throw err 170 182 } 171 - } 183 + }, []) 172 184 173 - const claimWispDomain = async (handle: string) => { 174 - try { 175 - const response = await fetch('/api/domain/claim', { 176 - method: 'POST', 177 - headers: { 'Content-Type': 'application/json' }, 178 - body: JSON.stringify({ handle }), 179 - }) 185 + const claimWispDomain = useCallback( 186 + async (handle: string) => { 187 + try { 188 + const response = await fetch('/api/domain/claim', { 189 + method: 'POST', 190 + headers: { 'Content-Type': 'application/json' }, 191 + body: JSON.stringify({ handle }), 192 + }) 180 193 181 - const data = await response.json() 182 - if (data.success) { 183 - await fetchDomains() 184 - return { success: true } 185 - } else { 186 - throw new Error(data.error || 'Failed to claim domain') 187 - } 188 - } catch (err) { 189 - console.error('Claim domain error:', err) 190 - const errorMessage = err instanceof Error ? err.message : 'Unknown error' 194 + const data = await response.json() 195 + if (data.success) { 196 + await fetchDomains() 197 + return { success: true } 198 + } else { 199 + throw new Error(data.error || 'Failed to claim domain') 200 + } 201 + } catch (err) { 202 + console.error('Claim domain error:', err) 203 + const errorMessage = err instanceof Error ? err.message : 'Unknown error' 191 204 192 - // Handle domain limit error more gracefully 193 - if (errorMessage.includes('Domain limit reached')) { 194 - alert('You have already claimed 3 wisp.place subdomains (maximum limit). Supporters get unlimited subdomains!') 195 - await fetchDomains() 196 - } else { 197 - alert(`Failed to claim domain: ${errorMessage}`) 205 + // Handle domain limit error more gracefully 206 + if (errorMessage.includes('Domain limit reached')) { 207 + alert( 208 + 'You have already claimed 3 wisp.place subdomains (maximum limit). Supporters get unlimited subdomains!', 209 + ) 210 + await fetchDomains() 211 + } else { 212 + alert(`Failed to claim domain: ${errorMessage}`) 213 + } 214 + return { success: false, error: errorMessage } 198 215 } 199 - return { success: false, error: errorMessage } 200 - } 201 - } 216 + }, 217 + [fetchDomains], 218 + ) 202 219 203 - const checkWispAvailability = async (handle: string) => { 220 + const checkWispAvailability = useCallback(async (handle: string) => { 204 221 const trimmedHandle = handle.trim().toLowerCase() 205 222 if (!trimmedHandle) { 206 223 return { available: null } ··· 214 231 console.error('Check availability error:', err) 215 232 return { available: false } 216 233 } 217 - } 234 + }, []) 218 235 219 236 return { 220 237 wispDomains,
+26 -23
apps/main-app/public/editor/hooks/useSiteData.ts
··· 1 - import { useState } from 'react' 1 + import { useCallback, useState } from 'react' 2 2 3 3 export interface Site { 4 4 did: string ··· 24 24 const [sitesLoading, setSitesLoading] = useState(true) 25 25 const [isSyncing, setIsSyncing] = useState(false) 26 26 27 - const fetchSites = async () => { 27 + const fetchSites = useCallback(async () => { 28 28 try { 29 29 const response = await fetch('/api/user/sites') 30 30 const data = await response.json() ··· 56 56 } finally { 57 57 setSitesLoading(false) 58 58 } 59 - } 59 + }, []) 60 60 61 - const syncSites = async () => { 61 + const syncSites = useCallback(async () => { 62 62 setIsSyncing(true) 63 63 try { 64 64 const response = await fetch('/api/user/sync', { ··· 76 76 } finally { 77 77 setIsSyncing(false) 78 78 } 79 - } 79 + }, [fetchSites]) 80 80 81 - const deleteSite = async (rkey: string) => { 82 - try { 83 - const response = await fetch(`/api/site/${rkey}`, { 84 - method: 'DELETE', 85 - }) 81 + const deleteSite = useCallback( 82 + async (rkey: string) => { 83 + try { 84 + const response = await fetch(`/api/site/${rkey}`, { 85 + method: 'DELETE', 86 + }) 86 87 87 - const data = await response.json() 88 - if (data.success) { 89 - // Refresh sites list 90 - await fetchSites() 91 - return true 92 - } else { 93 - throw new Error(data.error || 'Failed to delete site') 88 + const data = await response.json() 89 + if (data.success) { 90 + // Refresh sites list 91 + await fetchSites() 92 + return true 93 + } else { 94 + throw new Error(data.error || 'Failed to delete site') 95 + } 96 + } catch (err) { 97 + console.error('Delete site error:', err) 98 + alert(`Failed to delete site: ${err instanceof Error ? err.message : 'Unknown error'}`) 99 + return false 94 100 } 95 - } catch (err) { 96 - console.error('Delete site error:', err) 97 - alert(`Failed to delete site: ${err instanceof Error ? err.message : 'Unknown error'}`) 98 - return false 99 - } 100 - } 101 + }, 102 + [fetchSites], 103 + ) 101 104 102 105 return { 103 106 sites,
+3 -2
apps/main-app/public/editor/tabs/CLITab.tsx
··· 1 1 import { Badge } from '@public/components/ui/badge' 2 2 import { CodeBlock } from '@public/components/ui/code-block' 3 3 import { Download, ExternalLink } from 'lucide-react' 4 + import { memo } from 'react' 4 5 5 6 const BASE_URL = 'https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries' 6 7 ··· 44 45 ) 45 46 } 46 47 47 - export function CLITab() { 48 + export const CLITab = memo(function CLITab() { 48 49 return ( 49 50 <div className="h-full flex flex-col border border-border/30 bg-card/50 font-mono"> 50 51 {/* Header */} ··· 251 252 </div> 252 253 </div> 253 254 ) 254 - } 255 + })
+4 -4
apps/main-app/public/editor/tabs/DomainsTab.tsx
··· 12 12 import { Label } from '@public/components/ui/label' 13 13 import { SkeletonShimmer } from '@public/components/ui/skeleton' 14 14 import { AlertCircle, CheckCircle2, Loader2, Trash2, XCircle } from 'lucide-react' 15 - import { type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent, useEffect, useRef, useState } from 'react' 15 + import { type ChangeEvent, memo, type KeyboardEvent as ReactKeyboardEvent, useEffect, useRef, useState } from 'react' 16 16 import type { CustomDomain, WispDomain } from '../hooks/useDomainData' 17 17 import type { UserInfo } from '../hooks/useUserInfo' 18 18 ··· 42 42 <kbd className="px-2 py-1 bg-muted/50 rounded border border-border/50">{children}</kbd> 43 43 ) 44 44 45 - export function DomainsTab({ 45 + export const DomainsTab = memo(function DomainsTab({ 46 46 wispDomains, 47 47 customDomains, 48 48 domainsLoading, ··· 104 104 if (wasOpen && !isOpen) setTimeout(() => containerRef.current?.focus(), 50) 105 105 wasOpen = isOpen 106 106 }) 107 - observer.observe(document.body, { childList: true, subtree: true }) 107 + observer.observe(document.body, { childList: true }) 108 108 return () => observer.disconnect() 109 109 }, []) 110 110 ··· 762 762 </Dialog> 763 763 </> 764 764 ) 765 - } 765 + })
+10 -4
apps/main-app/public/editor/tabs/SitesTab.tsx
··· 2 2 import { Button } from '@public/components/ui/button' 3 3 import { SkeletonShimmer } from '@public/components/ui/skeleton' 4 4 import { ChevronDown, ChevronRight, ExternalLink, Globe, Settings as SettingsIcon, Trash2 } from 'lucide-react' 5 - import { useCallback, useEffect, useRef, useState } from 'react' 5 + import { memo, useCallback, useEffect, useRef, useState } from 'react' 6 6 import type { SiteWithDomains } from '../hooks/useSiteData' 7 7 import type { UserInfo } from '../hooks/useUserInfo' 8 8 ··· 32 32 <kbd className="px-2 py-1 bg-muted/50 rounded border border-border/50">{children}</kbd> 33 33 ) 34 34 35 - export function SitesTab({ sites, sitesLoading, userInfo, onConfigureSite, onDeleteSite }: SitesTabProps) { 35 + export const SitesTab = memo(function SitesTab({ 36 + sites, 37 + sitesLoading, 38 + userInfo, 39 + onConfigureSite, 40 + onDeleteSite, 41 + }: SitesTabProps) { 36 42 // State: only one site can be expanded at a time (null = none expanded) 37 43 const [expandedSiteKey, setExpandedSiteKey] = useState<string | null>(null) 38 44 const [focusedIndex, setFocusedIndex] = useState(0) ··· 95 101 wasDialogOpen = isDialogOpen 96 102 }) 97 103 98 - observer.observe(document.body, { childList: true, subtree: true }) 104 + observer.observe(document.body, { childList: true }) 99 105 100 106 return () => observer.disconnect() 101 107 }, []) ··· 385 391 </div> 386 392 </div> 387 393 ) 388 - } 394 + })
+3 -3
apps/main-app/public/editor/tabs/UploadTab.tsx
··· 2 2 import { Input } from '@public/components/ui/input' 3 3 import { Label } from '@public/components/ui/label' 4 4 import { AlertCircle, CheckCircle2, ChevronDown, ChevronUp, Loader2, RefreshCw, Upload, XCircle } from 'lucide-react' 5 - import { type ChangeEvent, useEffect, useRef, useState } from 'react' 5 + import { type ChangeEvent, memo, useEffect, useRef, useState } from 'react' 6 6 import type { SiteWithDomains } from '../hooks/useSiteData' 7 7 8 8 type FileStatus = 'pending' | 'checking' | 'uploading' | 'uploaded' | 'reused' | 'failed' ··· 19 19 onUploadComplete: () => Promise<void> 20 20 } 21 21 22 - export function UploadTab({ sites, sitesLoading, onUploadComplete }: UploadTabProps) { 22 + export const UploadTab = memo(function UploadTab({ sites, sitesLoading, onUploadComplete }: UploadTabProps) { 23 23 // Upload state 24 24 const [siteMode, setSiteMode] = useState<'existing' | 'new'>('existing') 25 25 const [selectedSiteRkey, setSelectedSiteRkey] = useState<string>('') ··· 661 661 </div> 662 662 </div> 663 663 ) 664 - } 664 + })
+3 -3
apps/main-app/public/editor/tabs/WebhooksTab.tsx
··· 18 18 Webhook, 19 19 X, 20 20 } from 'lucide-react' 21 - import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' 21 + import { type ChangeEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' 22 22 import type { WebhookEventLog, WebhookRecord } from '../hooks/useWebhookData' 23 23 24 24 const APPS = [ ··· 80 80 return new Date(dateStr).toLocaleDateString() 81 81 } 82 82 83 - export function WebhooksTab({ 83 + export const WebhooksTab = memo(function WebhooksTab({ 84 84 webhooks, 85 85 webhooksLoading, 86 86 eventLogs, ··· 895 895 </div> 896 896 </div> 897 897 ) 898 - } 898 + })