183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import { useActionState, useEffect, useState } from 'react'
|
|
import Swal from 'sweetalert2'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { updateProfile, type Profile, type ProfileFormState } from './actions'
|
|
|
|
function getSwalThemeOptions() {
|
|
const isDark = document.documentElement.classList.contains('dark')
|
|
return isDark
|
|
? { background: '#111827', color: '#f3f4f6' }
|
|
: { background: '#ffffff', color: '#111827' }
|
|
}
|
|
|
|
const MAX_AVATAR_BYTES = 5 * 1024 * 1024
|
|
|
|
function showToast(icon: 'success' | 'error', title: string) {
|
|
void Swal.fire({
|
|
...getSwalThemeOptions(),
|
|
toast: true,
|
|
position: 'top-end',
|
|
showConfirmButton: false,
|
|
timer: 2200,
|
|
timerProgressBar: true,
|
|
icon,
|
|
title,
|
|
})
|
|
}
|
|
|
|
function showErrorDialog(title: string, text?: string) {
|
|
void Swal.fire({
|
|
...getSwalThemeOptions(),
|
|
icon: 'error',
|
|
title,
|
|
text,
|
|
confirmButtonText: 'Tamam',
|
|
})
|
|
}
|
|
|
|
export default function ProfileClient({ initialProfile }: { initialProfile: Profile }) {
|
|
const [state, formAction, pending] = useActionState<ProfileFormState, FormData>(updateProfile, {})
|
|
const [avatarFilePreviewUrl, setAvatarFilePreviewUrl] = useState<string>('')
|
|
|
|
useEffect(() => {
|
|
if (state.success) showToast('success', 'Profil güncellendi')
|
|
}, [state.success])
|
|
|
|
useEffect(() => {
|
|
if (state.error) showToast('error', state.error)
|
|
}, [state.error])
|
|
|
|
const profile = state.profile ?? initialProfile
|
|
const avatarSrc = avatarFilePreviewUrl || profile.avatar_url || ''
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (avatarFilePreviewUrl.startsWith('blob:')) URL.revokeObjectURL(avatarFilePreviewUrl)
|
|
}
|
|
}, [avatarFilePreviewUrl])
|
|
|
|
function onAvatarFileChange(file: File | null) {
|
|
if (!file) {
|
|
console.log('[profile:avatar] dosya seçimi temizlendi')
|
|
setAvatarFilePreviewUrl('')
|
|
return
|
|
}
|
|
console.log('[profile:avatar] dosya seçildi (henüz yüklenmedi)', {
|
|
name: file.name,
|
|
type: file.type,
|
|
size: file.size,
|
|
})
|
|
if (!file.type.startsWith('image/')) {
|
|
showToast('error', 'Lütfen geçerli bir görsel seçin')
|
|
return
|
|
}
|
|
if (file.size > MAX_AVATAR_BYTES) {
|
|
showErrorDialog(
|
|
'Dosya çok büyük',
|
|
`Avatar en fazla 5 MB olabilir (seçilen: ${(file.size / (1024 * 1024)).toFixed(2)} MB).`,
|
|
)
|
|
return
|
|
}
|
|
const previewUrl = URL.createObjectURL(file)
|
|
setAvatarFilePreviewUrl(previewUrl)
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto w-full max-w-3xl p-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profilim</CardTitle>
|
|
<CardDescription>Ad, soyad ve avatar bilgilerinizi güncelleyin.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form
|
|
action={formAction}
|
|
encType="multipart/form-data"
|
|
className="space-y-4"
|
|
onSubmit={(event) => {
|
|
const fd = new FormData(event.currentTarget)
|
|
const f = fd.get('avatar')
|
|
if (f instanceof File && f.size > MAX_AVATAR_BYTES) {
|
|
event.preventDefault()
|
|
showErrorDialog(
|
|
'Dosya çok büyük',
|
|
`Avatar en fazla 5 MB olabilir (seçilen: ${(f.size / (1024 * 1024)).toFixed(2)} MB). Sunucu gönderim limiti de 5 MB.`,
|
|
)
|
|
return
|
|
}
|
|
const hidden = String(fd.get('avatar_url') ?? '')
|
|
console.log('[profile:avatar] form gönderiliyor', {
|
|
hasFile: f instanceof File && f.size > 0,
|
|
file:
|
|
f instanceof File && f.size > 0
|
|
? { name: f.name, type: f.type, size: f.size }
|
|
: null,
|
|
avatar_url_hidden_preview: hidden.slice(0, 120),
|
|
})
|
|
}}
|
|
>
|
|
<div className="space-y-3 rounded-lg border p-4">
|
|
<Label htmlFor="avatar">Avatar</Label>
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
<Avatar data-size="lg" className="size-16">
|
|
<AvatarImage src={avatarSrc || undefined} alt={`${profile.first_name} ${profile.last_name}`} />
|
|
<AvatarFallback>
|
|
{(profile.first_name?.[0] ?? 'U') + (profile.last_name?.[0] ?? '')}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 space-y-2">
|
|
<Input
|
|
id="avatar"
|
|
name="avatar"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={(event) => void onAvatarFileChange(event.target.files?.[0] ?? null)}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Görsel, form submit edildiğinde sunucuya yüklenir ve profile kaydedilir.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="first_name">Ad</Label>
|
|
<Input id="first_name" name="first_name" defaultValue={profile.first_name ?? ''} required />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="last_name">Soyad</Label>
|
|
<Input id="last_name" name="last_name" defaultValue={profile.last_name ?? ''} required />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="avatar_manual">Avatar URL (Opsiyonel)</Label>
|
|
<Input
|
|
id="avatar_manual"
|
|
name="avatar_url"
|
|
key={profile.avatar_url ?? ''}
|
|
type="url"
|
|
defaultValue={profile.avatar_url ?? ''}
|
|
placeholder="https://..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? 'Kaydediliyor…' : 'Profili Kaydet'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|