@@ -432,10 +432,6 @@ const CRON_PRESETS = [
{ label : 'Her Pazartesi' , value : '0 0 * * 1' } ,
]
function extractGdriveEmail ( json : string ) : string | null {
try { return JSON . parse ( json ) ? . client_email ? ? null } catch { return null }
}
function BackupTab ( { db } : { db : Db } ) {
const [ config , setConfig ] = useState < any > ( null )
const [ logs , setLogs ] = useState < any [ ] > ( [ ] )
@@ -443,8 +439,9 @@ function BackupTab({ db }: { db: Db }) {
const [ saving , setSaving ] = useState ( false )
const [ testing , setTesting ] = useState ( false )
const [ testResult , setTestResult ] = useState < { ok : boolean ; detail? : string ; error? : string } | null > ( null )
const [ copied , setCopied ] = useState ( false )
const [ f , setF ] = useState ( { schedule : '0 0 * * *' , cloud_type : 'gdrive' , credentials : '' , gdrive_folder_id : '' } )
const [ copiedUri , setCopiedUri ] = useState ( false )
const [ redirectUri , setRedirectUri ] = useState ( '' )
const [ f , setF ] = useState ( { schedule : '0 0 * * *' , cloud_type : 'gcs' , credentials : '' , gdrive_folder_id : '' } )
const load = useCallback ( async ( ) = > {
setLoading ( true )
@@ -453,37 +450,56 @@ function BackupTab({ db }: { db: Db }) {
const data = await r . json ( )
setConfig ( data . config )
setLogs ( data . logs )
if ( data . config ) {
setF ( { schedule : data.config.schedule , cloud_type : data.config.cloud_type , credentials : data.config.credentials , gdrive_folder_id : data.config.gdrive_folder_id || '' } )
}
if ( data . config ) setF ( { schedule : data.config.schedule , cloud_type : data.config.cloud_type , credentials : data.config.credentials , gdrive_folder_id : data.config.gdrive_folder_id || '' } )
}
setLoading ( false )
} , [ db . id ] )
useEffect ( ( ) = > { load ( ) } , [ load ] )
useEffect ( ( ) = > {
load ( )
setRedirectUri ( window . location . origin + '/api/backups/gdrive/callback' )
// URL param kontrol — Google OAuth callback sonrası
const params = new URLSearchParams ( window . location . search )
if ( params . get ( 'gdrive_connected' ) === db . id ) {
window . history . replaceState ( { } , '' , window . location . pathname )
load ( )
} else if ( params . get ( 'gdrive_error' ) ) {
setTestResult ( { ok : false , error : ` Google bağlantı hatası : ${ params . get ( 'gdrive_error' ) } ` } )
window . history . replaceState ( { } , '' , window . location . pathname )
}
} , [ load , db . id ] )
// gdrive credentials yardı mcı ları
const gdriveFields = ( ( ) = > {
try { const c = JSON . parse ( f . credentials ) ; return { client_id : c.client_id || '' , client_secret : c.client_secret || '' , connected : ! ! c . refresh_token } }
catch { return { client_id : '' , client_secret : '' , connected : false } }
} ) ( )
const updateGdriveField = ( key : string , val : string ) = > {
try { const c = JSON . parse ( f . credentials ) ; setF ( p = > ( { . . . p , credentials : JSON.stringify ( { . . . c , [ key ] : val } ) } ) ) }
catch { setF ( p = > ( { . . . p , credentials : JSON.stringify ( { [ key ] : val } ) } ) ) }
}
const saveConfig = async ( ) = > {
setSaving ( true )
await fetch ( '/api/backups' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { db_id : db.id , . . . f } ) ,
} )
await fetch ( '/api/backups' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { db_id : db.id , . . . f } ) } )
await load ( )
setSaving ( false )
}
const testCredentials = async ( ) = > {
setTesting ( true )
setTestResult ( null )
const r = await fetch ( '/api/backups/test' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( { db_id : db.id , cloud_type : f.cloud_type , credentials : f.credentials , gdrive_folder_id : f.gdrive_folder_id } ) ,
} )
const connectGDrive = async ( ) = > {
setSaving ( true )
await fetch ( '/api/backups' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { db_id : db.id , . . . f } ) } )
setSaving ( false )
window . location . href = ` /api/backups/gdrive/connect?db_id= ${ db . id } `
}
const testNow = async ( ) = > {
setTesting ( true ) ; setTestResult ( null )
const r = await fetch ( '/api/backups/test' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { db_id : db.id , cloud_type : f.cloud_type , credentials : f.credentials , gdrive_folder_id : f.gdrive_folder_id } ) } )
const result = await r . json ( )
setTestResult ( result )
if ( result . ok ) await load ( ) // log listesini güncelle
if ( result . ok ) await load ( )
setTesting ( false )
}
@@ -491,17 +507,14 @@ function BackupTab({ db }: { db: Db }) {
if ( ! confirm ( 'Otomatik yedeklemeyi kapat?' ) ) return
await fetch ( '/api/backups' , { method : 'DELETE' , headers : { 'Content-Type' : 'application/json' } , body : JSON.stringify ( { db_id : db.id } ) } )
setConfig ( null )
setF ( { schedule : '0 0 * * *' , cloud_type : 'gdrive ' , credentials : '' , gdrive_folder_id : '' } )
}
const serviceEmail = f . cloud_type === 'gdrive' ? extractGdriveEmail ( f . credentials ) : null
const copyEmail = ( ) = > {
if ( serviceEmail ) { navigator . clipboard . writeText ( serviceEmail ) ; setCopied ( true ) ; setTimeout ( ( ) = > setCopied ( false ) , 2000 ) }
setF ( { schedule : '0 0 * * *' , cloud_type : 'gcs ' , credentials : '' , gdrive_folder_id : '' } )
}
if ( loading ) return < div className = "text-muted text-sm font-mono animate-pulse" > Yükleniyor . . . < / div >
const isGdrive = f . cloud_type === 'gdrive'
const credentialsReady = isGdrive ? gdriveFields . connected : ! ! f . credentials . trim ( )
return (
< div className = "flex gap-6 items-start fade-up" >
< div className = "flex-1 space-y-4" >
@@ -513,17 +526,12 @@ function BackupTab({ db }: { db: Db }) {
< div className = "text-sm font-semibold mb-0.5" > Manuel Yedek Al < / div >
< div className = "text-[11px] text-muted font-mono" > SQL dump olarak indir < / div >
< / div >
< a
href = { ` /api/db/backup?dbId= ${ db . id } ` }
download
className = "inline-flex items-center justify-center gap-1.5 rounded-md text-xs font-medium border border-border bg-transparent hover:bg-surface-2 text-text h-8 px-3 transition-colors"
>
< a href = { ` /api/db/backup?dbId= ${ db . id } ` } download className = "inline-flex items-center gap-1.5 rounded-md text-xs font-medium border border-border bg-transparent hover:bg-surface-2 text-text h-8 px-3 transition-colors" >
< Download className = "w-3.5 h-3.5" / > İ ndir
< / a >
< / CardContent >
< / Card >
{ /* Cloud ayarları */ }
< Card >
< CardHeader className = "pb-4" >
< CardTitle className = "text-base" > Otomatik Yedekleme < / CardTitle >
@@ -531,151 +539,153 @@ function BackupTab({ db }: { db: Db }) {
< / CardHeader >
< CardContent className = "space-y-5" >
{ /* Provider */ }
{ /* Provider seçimi */ }
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Depolama < / label >
< div className = "flex gap-2" >
{ [
{ id : 'gdrive ', label : '📁 Google Drive' } ,
{ id : 'dropbox' , label : '📦 Dropbox' } ,
] . map ( p = > (
< button
key = { p . id }
onClick = { ( ) = > { setF ( prev = > ( { . . . prev , cloud_type : p.id } ) ) ; setTestResult ( null ) } }
className = { ` flex-1 py-2 rounded-lg border text-sm font-medium transition-all ${
f . cloud_type === p . id
? 'bg-accent/10 border-accent/40 text-accent'
: 'border-border/50 text-muted hover:text-text hover:border-border'
} ` }
>
{ [ { id : 'gcs' , label : '☁️ Google Cloud Storage' } , { id : 'dropbox' , label : '📦 Dropbox' } , { id : 'gdrive' , label : '📁 Google Drive' } ] . map ( p = > (
< button key = { p . id } onClick = { ( ) = > { setF ( prev = > ( { . . . prev , cloud_type : p.id , credentials : ' ', gdrive_folder_id : '' } ) ) ; setTestResult ( null ) } }
className = { ` flex-1 py-2 rounded-lg border text-xs font-medium transition-all ${ f . cloud_type === p . id ? 'bg-accent/10 border-accent/40 text-accent' : 'border-border/50 text-muted hover:text-text hover:border-border' } ` } >
{ p . label }
< / button >
) ) }
< / div >
< / div >
{ /* Google Drive kurulum rehberi */ }
{ f . cloud_type === 'gdrive ' && (
< div className = "rounded-xl border border-border/50 bg-black/20 p-4 space-y-3" >
< div className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Google Drive Kurulumu < / div >
{ [
{ /* ── GCS ─────────────────────────────────────────────────────── */ }
{ f . cloud_type === 'gcs ' && (
< >
< StepGuide steps = { [
{ n : 1 , text : 'console.cloud.google.com → Proje seç/oluştur' } ,
{ n : 2 , text : '"APIs & Services" → "Enabl e APIs " → "Google Drive API" aç ' } ,
{ n : 3 , text : '"Credentials" → "Service Accoun ts" → Yeni hesap oluştur → JSON key indir ' } ,
{ n : 4 , text : 'Google Drive\'da bir klasör oluştur (örn. "VPS Backups")' } ,
{ n : 5 , text : 'Klasörü sağ tı kla → "Paylaş" → aşağıdaki service account e-postası nı "Düzenleyici" olarak ekle ' , highlight : true } ,
{ n : 6 , text : 'Klasörü aç → URL\'deki /folders/XXXXX kı smı nı Klasör ID alanı na yapıştı r' , highlight : true } ,
] . map ( s = > (
< div key = { s . n } className = { ` flex gap-3 text-[11px] font-mono leading-relaxed ${ ( s as any ) . highlight ? 'bg-yellow-500/5 border border-yellow-500/20 rounded-lg px-2 py-1.5 -mx-2' : '' } ` } >
< span className = { ` w-5 h-5 rounded-full border flex items-center justify-center text-[10px] shrink-0 mt-0.5 ${ ( s as any ) . highlight ? 'bg-yellow-500/20 border-yellow-500/40 text-yellow-400' : 'bg-accent/10 border-accent/20 text-accent' } ` } > { s . n } < / span >
< span className = { ( s as any ) . highlight ? 'text-yellow-200/80' : 'text-muted/80' } > { s . text } < / span >
< / div >
) ) }
< / div >
) }
{ /* Credentials */ }
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" >
{ f . cloud_type === 'gdrive' ? 'Service Account JSON' : 'Dropbox Access Token' }
< / label >
< textarea
className = "flex w-full rounded-md border border-border bg-black/30 px-3 py-2 text-xs shadow-sm transition-colors placeholder:text-muted/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent font-mono resize-y min-h-[120px]"
value = { f . credentials }
onChange = { e = > { setF ( p = > ( { . . . p , credentials : e.target.value } ) ) ; setTestResult ( null ) } }
placeholder = { f . cloud_type === 'gdrive'
? '{\n "type": "service_account",\n "project_id": "...",\n "private_key": "-----BEGIN RSA PRIVATE KEY-----\\n...",\n "client_email": "xxx@project.iam.gserviceaccount.com",\n ...\n}'
: 'sl.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' }
/ >
< / div >
{ /* Extracted email banner */ }
{ serviceEmail && (
< div className = "flex items-center justify-between gap-3 rounded-lg border border-accent/20 bg-accent/5 px-4 py-3" >
< div >
< div className = "text-[10px] text-muted font-mono uppercase tracking-wider mb-1" > Bu e - postayı Drive klasörünüzle paylaşın < / div >
< div className = "text-sm font-mono text-accent" > { serviceEmail } < / div >
{ n : 2 , text : '"APIs & Services" → "Cloud Storag e API" → Enable ' } ,
{ n : 3 , text : '"Cloud Storage" → "Bucke ts" → Yeni bucket oluştur → adı nı not et ' } ,
{ n : 4 , text : '"IAM & Admin" → "Service Accounts" → Hesap oluştur → JSON key indir' , hi : true } ,
{ n : 5 , text : 'Bucket → "Permissions" → "Grant Access" → service account e-postası → "Storage Object Admin" ' , hi : true } ,
{ n : 6 , text : 'Bucket adı nı aşağıya gir, JSON\'ı yapıştı r → Şimdi Yedekle' } ,
] } / >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Bucket Adı < / label >
< Input value = { f . gdrive_folder_id } onChange = { e = > setF ( p = > ( { . . . p , gdrive_folder_id : e.target.value } ) ) } placeholder = "vps-backups" / >
< / div >
< button
onClick = { copyEmail }
className = "p-1.5 rounded text-muted hover:text-accent transition-colors shrink-0"
title = "Kopyala"
>
{ copied ? < CheckCircle2 className = "w-4 h-4 text-success" / > : < Copy className = "w-4 h-4" / > }
< / button >
< / div >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Service Account JSON < / label >
< textarea className = "flex w-full rounded-md border border-border bg-black/30 px-3 py-2 text-xs font-mono resize-y min-h-[100px] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent" value = { f . credentials } onChange = { e = > { setF ( p = > ( { . . . p , credentials : e.target.value } ) ) ; setTestResult ( null ) } } placeholder = { '{\n "type": "service_account",\n "client_email": "xxx@project.iam.gserviceaccount.com",\n ...\n}' } / >
< / div >
< / >
) }
{ /* Klasör ID */ }
{ f . cloud_type === 'g drive ' && (
{ /* ── Dropbox ──────────────────────────────────────────────────── */ }
{ f . cloud_type === 'dropbox ' && (
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Klasör ID < / label >
< Input
value = { f . gdrive_folder_id }
onChange = { e = > { setF ( p = > ( { . . . p , gdrive_folder_id : e.target.value } ) ) ; setTestResult ( null ) } }
placeholder = "1A2b3C4d5E6f7G8h9I0jKlMnOpQrStUvWx"
/ >
< div className = "text-[10px] text-muted/60 font-mono" >
Drive klasörünü açınca URL 'deki <span className="text-muted">/folders/</span> sonrası ndaki kı sı m
</div>
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Access Token < / label >
< Input value = { f . credentials } onChange = { e = > { setF ( p = > ( { . . . p , credentials : e.target.value } ) ) ; setTestResult ( null ) } } placeholder = "sl.xxxxxxxxxxxxxxxx..." / >
< div className = "text-[10px] text-muted/60 font-mono" > dropbox . com / developers → Apps → Create app → Generate token < / div >
< / div >
) }
{ /* ── Google Drive OAuth ───────────────────────────────────────── */ }
{ isGdrive && (
< >
{ gdriveFields . connected ? (
/* Bağlı durumu */
< div className = "flex items-center justify-between rounded-xl border border-success/30 bg-success/5 px-4 py-3" >
< div className = "flex items-center gap-2.5" >
< CheckCircle2 className = "w-4 h-4 text-success" / >
< div >
< div className = "text-sm font-semibold text-success" > Google hesabı bağlı < / div >
< div className = "text-[10px] text-muted font-mono mt-0.5" > OAuth refresh token aktif < / div >
< / div >
< / div >
< button onClick = { connectGDrive } className = "text-[10px] font-mono text-muted hover:text-text border border-border rounded px-2 py-1 transition-colors" >
Yeniden Bağla
< / button >
< / div >
) : (
/* Bağlantı kurulum formu */
< >
< StepGuide steps = { [
{ n : 1 , text : 'console.cloud.google.com → Proje seç/oluştur' } ,
{ n : 2 , text : '"APIs & Services" → "Enable APIs" → "Google Drive API" aç' } ,
{ n : 3 , text : '"Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs" → Web application seç' } ,
{ n : 4 , text : '"Authorized redirect URIs" → aşağıdaki URL\'yi ekle' , hi : true } ,
{ n : 5 , text : 'Client ID ve Client Secret\'i kopyala → aşağıya yapıştı r' , hi : true } ,
{ n : 6 , text : '"Google ile Bağlan" a tı kla → izin ver → otomatik geri dön' } ,
] } / >
{ /* Redirect URI göster */ }
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Authorized Redirect URI ( Google Console 'a ekle)</label>
<div className="flex items-center gap-2">
<div className="flex-1 rounded-md border border-accent/30 bg-accent/5 px-3 py-2 text-xs font-mono text-accent truncate">{redirectUri}</div>
<button onClick={() => { navigator.clipboard.writeText(redirectUri); setCopiedUri(true); setTimeout(() => setCopiedUri(false), 2000) }}
className="p-2 rounded border border-border hover:bg-surface-2 text-muted hover:text-text transition-colors shrink-0">
{copiedUri ? <CheckCircle2 className="w-3.5 h-3.5 text-success" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-[10px] font-mono text-muted uppercase tracking-wider">Client ID</label>
<Input value={gdriveFields.client_id} onChange={e => updateGdriveField(' client_id ', e.target.value)} placeholder="xxxxxxx.apps.googleusercontent.com" />
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-mono text-muted uppercase tracking-wider">Client Secret</label>
<Input type="password" value={gdriveFields.client_secret} onChange={e => updateGdriveField(' client_secret ', e.target.value)} placeholder="GOCSPX-..." />
</div>
</div>
</>
)}
{/* Klasör ID — her zaman göster */}
<div className="space-y-1.5">
<label className="text-[10px] font-mono text-muted uppercase tracking-wider">Klasör ID (Opsiyonel)</label>
<Input value={f.gdrive_folder_id} onChange={e => setF(p => ({ ...p, gdrive_folder_id: e.target.value }))} placeholder="1A2b3C4d5E6f7G8h9I0j..." />
<div className="text-[10px] text-muted/60 font-mono">Belirtilmezse My Drive köküne yükler. Klasör URL' sindeki / folders / ID kı smı . < / div >
< / div >
< / >
) }
{ /* Test sonucu */ }
{ testResult && (
<div className={` flex items-start gap-2.5 rounded-lg border px-4 py-3 text-xs font-mono ${
testResult.ok
? ' border - success / 30 bg - success / 5 text - success '
: ' border - destructive / 30 bg - destructive / 5 text - destructive '
}`}>
{testResult.ok
? <CheckCircle2 className="w-4 h-4 shrink-0 mt-0.5" />
: <ShieldAlert className="w-4 h-4 shrink-0 mt-0.5" />}
< div className = { ` flex items-start gap-2.5 rounded-lg border px-4 py-3 text-xs font-mono ${ testResult . ok ? 'border-success/30 bg-success/5 text-success' : 'border-destructive/30 bg-destructive/5 text-destructive' } ` } >
{ testResult . ok ? < CheckCircle2 className = "w-4 h-4 shrink-0 mt-0.5" / > : < ShieldAlert className = "w-4 h-4 shrink-0 mt-0.5" / > }
< span > { testResult . ok ? testResult.detail : testResult.error } < / span >
< / div >
) }
{/* Schedule */}
{ /* Zamanlama */ }
< div className = "space-y-1.5" >
< label className = "text-[10px] font-mono text-muted uppercase tracking-wider" > Zamanlama < / label >
< div className = "flex flex-wrap gap-1.5 mb-2" >
{ CRON_PRESETS . map ( p = > (
<button
key={p.value}
onClick={() => setF(prev => ({ ...prev, schedule: p.value }))}
className={`text-[10px] font-mono px-2.5 py-1 rounded-md border transition-all ${
f.schedule === p.value
? ' border - accent / 40 bg - accent / 10 text - accent '
: ' border - border / 50 text - muted hover :text - text hover :border - border '
}`}
>
< button key = { p . value } onClick = { ( ) = > setF ( prev = > ( { . . . prev , schedule : p.value } ) ) }
className = { ` text-[10px] font-mono px-2.5 py-1 rounded-md border transition-all ${ f . schedule === p . value ? 'border-accent/40 bg-accent/10 text-accent' : 'border-border/50 text-muted hover:text-text hover:border-border' } ` } >
{ p . label }
< / button >
) ) }
< / div >
< Input value = { f . schedule } onChange = { e = > setF ( p = > ( { . . . p , schedule : e.target.value } ) ) } placeholder = "0 0 * * *" className = "font-mono text-sm" / >
<div className="text-[10px] text-muted/60 font-mono">cron format: dakika saat gün ay haftaGünü</div>
< / div >
{ /* Butonlar */ }
< div className = "flex gap-2 pt-1" >
<Button
variant="outline"
onClick={testCredentials}
disabled={testing || !f.credentials}
className="gap-1.5"
title="Şimdi yedek al ve buluta yükle"
>
{testing
? <Loader2 className= "w-3.5 h-3.5 animate-spin" />
: <FlaskConical className="w-3.5 h-3.5" />}
{testing ? ' Yedekleniyor . . . ' : ' Ş imdi Yedekle '}
</Button>
<Button onClick={saveConfig} disabled={saving || !f.credentials} className="flex-1">
{saving ? ' Kaydediliyor . . . ' : config ? ' Güncelle ' : ' Etkinleştir '}
</Button>
{config && (
<Button variant="danger" onClick={deleteConfig}>Kapat</Button>
{ isGdrive && ! gdriveFields . connected ? (
< Button onClick = { connectGDrive } disabled = { saving || ! gdriveFields . client_id || ! gdriveFields . client_secret } className = "flex-1 gap-1.5" >
{ saving ? < Loader2 className = "w-3.5 h-3.5 animate-spin" / > : null }
{ saving ? 'Kaydediliyor...' : 'Google ile Bağlan →' }
< / Button >
) : (
< >
< Button variant = "outline" onClick = { testNow } disabled = { testing || ! credentialsReady } className = "gap-1.5" title = "Şimdi yedek al ve yükle" >
{ testing ? < Loader2 className = "w-3.5 h-3.5 animate-spin" / > : < FlaskConical className = "w-3.5 h-3.5" / > }
{ testing ? 'Yedekleniyor...' : 'Şimdi Yedekle' }
< / Button >
< Button onClick = { saveConfig } disabled = { saving || ! credentialsReady } className = "flex-1" >
{ saving ? 'Kaydediliyor...' : config ? 'Güncelle' : 'Etkinleştir' }
< / Button >
{ config && < Button variant = "danger" onClick = { deleteConfig } > Kapat < / Button > }
< / >
) }
< / div >
< / CardContent >
@@ -730,6 +740,19 @@ function BackupTab({ db }: { db: Db }) {
)
}
function StepGuide ( { steps } : { steps : { n : number ; text : string ; hi? : boolean } [ ] } ) {
return (
< div className = "rounded-xl border border-border/50 bg-black/20 p-4 space-y-3" >
{ steps . map ( s = > (
< div key = { s . n } className = { ` flex gap-3 text-[11px] font-mono leading-relaxed ${ s . hi ? 'bg-yellow-500/5 border border-yellow-500/20 rounded-lg px-2 py-1.5 -mx-2' : '' } ` } >
< span className = { ` w-5 h-5 rounded-full border flex items-center justify-center text-[10px] shrink-0 mt-0.5 ${ s . hi ? 'bg-yellow-500/20 border-yellow-500/40 text-yellow-400' : 'bg-accent/10 border-accent/20 text-accent' } ` } > { s . n } < / span >
< span className = { s . hi ? 'text-yellow-200/80' : 'text-muted/80' } > { s . text } < / span >
< / div >
) ) }
< / div >
)
}
function ActivityIcon ( props : any ) {
return (
< svg