315 lines
8.2 KiB
Markdown
315 lines
8.2 KiB
Markdown
# Admin Panel Resim Yönetimi Güncellemeleri
|
||
|
||
## Yapılan Değişiklikler
|
||
|
||
### 1. Site Ayarları - Resim Yükleme Hatası Düzeltildi
|
||
|
||
**Sorun:** Admin panelinden hero görseli yüklenirken "Cannot coerce the result to a single JSON object" hatası alınıyordu.
|
||
|
||
**Çözüm:** `src/db/api.ts` dosyasındaki `siteSettingsApi.update()` fonksiyonu güncellendi:
|
||
|
||
```typescript
|
||
// Önce ayarın var olup olmadığını kontrol et
|
||
const { data: existing } = await supabase
|
||
.from('site_settings')
|
||
.select('id')
|
||
.eq('key', key)
|
||
.maybeSingle();
|
||
|
||
if (existing) {
|
||
// Güncelle
|
||
const { data, error } = await supabase
|
||
.from('site_settings')
|
||
.update({ value, updated_at: new Date().toISOString() })
|
||
.eq('key', key)
|
||
.select()
|
||
.maybeSingle(); // .single() yerine .maybeSingle() kullanıldı
|
||
|
||
if (error) throw error;
|
||
return data;
|
||
} else {
|
||
// Yeni oluştur
|
||
const { data, error } = await supabase
|
||
.from('site_settings')
|
||
.insert({ key, value })
|
||
.select()
|
||
.maybeSingle();
|
||
|
||
if (error) throw error;
|
||
return data;
|
||
}
|
||
```
|
||
|
||
**Değişiklikler:**
|
||
- `.single()` yerine `.maybeSingle()` kullanıldı (daha güvenli)
|
||
- Ayar yoksa otomatik olarak oluşturuluyor
|
||
- `hero_image` ayarı veritabanına eklendi
|
||
|
||
### 2. Places (Yerler) - Resim Yükleme/Silme/Güncelleme Eklendi
|
||
|
||
**Yeni Özellikler:**
|
||
|
||
#### a) Resim Yükleme
|
||
- Dosya seçici ile resim yükleme
|
||
- 1MB boyut sınırı kontrolü
|
||
- Sadece resim dosyaları (image/*) kabul edilir
|
||
- Otomatik önizleme gösterimi
|
||
- Yüklenen resim URL'i otomatik olarak form alanına eklenir
|
||
|
||
#### b) Resim Silme
|
||
- Yüklenen resmi X butonu ile silme
|
||
- Storage'dan da otomatik silme
|
||
- Form alanını temizleme
|
||
|
||
#### c) Resim Güncelleme
|
||
- Mevcut resmi gösterme
|
||
- Yeni resim yükleyerek güncelleme
|
||
- Manuel URL girişi de desteklenir
|
||
|
||
**Kod Değişiklikleri:**
|
||
|
||
```typescript
|
||
// Yeni state'ler eklendi
|
||
const [uploadingImage, setUploadingImage] = useState(false);
|
||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||
|
||
// Resim yükleme fonksiyonu
|
||
const handleImageUpload = async (file: File) => {
|
||
// Validasyon
|
||
if (file.size > 1024 * 1024) {
|
||
toast({ title: 'Hata', description: 'Dosya boyutu 1MB\'dan küçük olmalıdır.' });
|
||
return null;
|
||
}
|
||
|
||
if (!file.type.startsWith('image/')) {
|
||
toast({ title: 'Hata', description: 'Sadece resim dosyaları yüklenebilir.' });
|
||
return null;
|
||
}
|
||
|
||
// Storage'a yükle
|
||
const fileExt = file.name.split('.').pop();
|
||
const fileName = `place-${Date.now()}.${fileExt}`;
|
||
const filePath = `places/${fileName}`;
|
||
|
||
const { data, error } = await supabase.storage
|
||
.from('site-assets')
|
||
.upload(filePath, file, {
|
||
cacheControl: '3600',
|
||
upsert: false
|
||
});
|
||
|
||
if (error) throw error;
|
||
|
||
// Public URL al
|
||
const { data: { publicUrl } } = supabase.storage
|
||
.from('site-assets')
|
||
.getPublicUrl(filePath);
|
||
|
||
setImagePreview(publicUrl);
|
||
form.setValue('image_url', publicUrl);
|
||
|
||
return publicUrl;
|
||
};
|
||
|
||
// Resim silme fonksiyonu
|
||
const handleRemoveImage = async () => {
|
||
const currentImageUrl = form.getValues('image_url');
|
||
|
||
if (currentImageUrl && currentImageUrl.includes('site-assets')) {
|
||
const urlParts = currentImageUrl.split('/');
|
||
const fileName = urlParts[urlParts.length - 1];
|
||
const filePath = `places/${fileName}`;
|
||
|
||
await supabase.storage
|
||
.from('site-assets')
|
||
.remove([filePath]);
|
||
}
|
||
|
||
setImagePreview(null);
|
||
form.setValue('image_url', '');
|
||
};
|
||
```
|
||
|
||
**UI Değişiklikleri:**
|
||
|
||
Form içine resim yükleme bölümü eklendi:
|
||
|
||
```tsx
|
||
<div className="space-y-2">
|
||
<Label>Yer Görseli</Label>
|
||
<div className="space-y-4">
|
||
{/* Önizleme */}
|
||
{imagePreview && (
|
||
<div className="relative w-full h-48 rounded-lg border overflow-hidden bg-muted">
|
||
<img src={imagePreview} alt="Önizleme" className="w-full h-full object-cover" />
|
||
<Button
|
||
type="button"
|
||
variant="destructive"
|
||
size="icon"
|
||
className="absolute top-2 right-2"
|
||
onClick={handleRemoveImage}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Dosya Seçici */}
|
||
<div className="flex items-center gap-4">
|
||
<Input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) handleImageUpload(file);
|
||
}}
|
||
disabled={uploadingImage}
|
||
className="cursor-pointer"
|
||
/>
|
||
{uploadingImage && <Loader2 className="h-5 w-5 animate-spin text-primary" />}
|
||
</div>
|
||
|
||
<p className="text-xs text-muted-foreground">
|
||
PNG, JPG veya WEBP. Maksimum 1MB.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Manuel URL Girişi */}
|
||
<FormField
|
||
control={form.control}
|
||
name="image_url"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Görsel URL (Manuel)</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="https://..." {...field} />
|
||
</FormControl>
|
||
<p className="text-xs text-muted-foreground">
|
||
Yukarıdan resim yükleyebilir veya buraya manuel URL girebilirsiniz.
|
||
</p>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
```
|
||
|
||
### 3. Storage Yapısı
|
||
|
||
**site-assets** bucket'ı içinde klasör yapısı:
|
||
|
||
```
|
||
site-assets/
|
||
├── places/ (Yer resimleri)
|
||
│ ├── place-1234567890.jpg
|
||
│ ├── place-1234567891.png
|
||
│ └── ...
|
||
├── site_logo-* (Site logosu)
|
||
├── header_background-* (Header arka plan)
|
||
└── hero_image-* (Hero görseli)
|
||
```
|
||
|
||
### 4. Güvenlik ve Validasyon
|
||
|
||
**Dosya Validasyonu:**
|
||
- ✅ Maksimum 1MB boyut sınırı
|
||
- ✅ Sadece resim dosyaları (image/*)
|
||
- ✅ Dosya tipi kontrolü
|
||
- ✅ Hata mesajları kullanıcıya gösteriliyor
|
||
|
||
**Storage Güvenliği:**
|
||
- ✅ Authenticated kullanıcılar yükleyebilir
|
||
- ✅ Admin rolü gerekli
|
||
- ✅ Public okuma erişimi var
|
||
- ✅ Dosya isimleri timestamp ile unique
|
||
|
||
## Kullanım
|
||
|
||
### Site Ayarları - Resim Yükleme
|
||
|
||
1. Admin panelinden **Ayarlar** sayfasına gidin
|
||
2. **Site Görünümü** bölümünde istediğiniz resim alanını bulun:
|
||
- Site Logosu
|
||
- Header Arka Plan Resmi
|
||
- Ana Sayfa Hero Görseli
|
||
3. **Dosya Seç** butonuna tıklayın
|
||
4. 1MB'dan küçük bir resim seçin
|
||
5. Resim otomatik olarak yüklenecek ve önizleme gösterilecek
|
||
|
||
### Places - Resim Yönetimi
|
||
|
||
1. Admin panelinden **Yerler** sayfasına gidin
|
||
2. Yeni yer eklemek için **Yeni Yer Ekle** butonuna tıklayın
|
||
3. Form içinde **Yer Görseli** bölümünü bulun
|
||
4. İki seçenek var:
|
||
- **Dosya Yükle:** Bilgisayarınızdan resim seçin
|
||
- **Manuel URL:** Harici bir resim URL'i girin
|
||
5. Resim yüklendikten sonra önizleme gösterilir
|
||
6. Resmi silmek için sağ üstteki **X** butonuna tıklayın
|
||
7. Formu kaydettiğinizde resim URL'i veritabanına kaydedilir
|
||
|
||
### Mevcut Yeri Düzenleme
|
||
|
||
1. Yer listesinde düzenlemek istediğiniz yerin yanındaki **Düzenle** butonuna tıklayın
|
||
2. Mevcut resim varsa önizleme gösterilir
|
||
3. Yeni resim yükleyerek güncelleyebilirsiniz
|
||
4. Veya mevcut resmi silip yeni bir URL girebilirsiniz
|
||
|
||
## Teknik Detaylar
|
||
|
||
### Dosya İsimlendirme
|
||
|
||
```typescript
|
||
// Site ayarları için
|
||
const fileName = `${key}-${Date.now()}.${fileExt}`;
|
||
// Örnek: hero_image-1707303456789.jpg
|
||
|
||
// Places için
|
||
const fileName = `place-${Date.now()}.${fileExt}`;
|
||
// Örnek: place-1707303456789.jpg
|
||
```
|
||
|
||
### Storage Yolu
|
||
|
||
```typescript
|
||
// Site ayarları için
|
||
const filePath = `${fileName}`; // Root seviyede
|
||
|
||
// Places için
|
||
const filePath = `places/${fileName}`; // places/ klasöründe
|
||
```
|
||
|
||
### Public URL Alma
|
||
|
||
```typescript
|
||
const { data: { publicUrl } } = supabase.storage
|
||
.from('site-assets')
|
||
.getPublicUrl(filePath);
|
||
|
||
// Örnek URL:
|
||
// https://[project-id].supabase.co/storage/v1/object/public/site-assets/places/place-1707303456789.jpg
|
||
```
|
||
|
||
## Test Edildi
|
||
|
||
- ✅ Site logosu yükleme
|
||
- ✅ Header arka plan yükleme
|
||
- ✅ Hero görseli yükleme
|
||
- ✅ Place resmi yükleme
|
||
- ✅ Resim silme
|
||
- ✅ Resim güncelleme
|
||
- ✅ Manuel URL girişi
|
||
- ✅ Dosya boyutu validasyonu
|
||
- ✅ Dosya tipi validasyonu
|
||
- ✅ Önizleme gösterimi
|
||
- ✅ Hata mesajları
|
||
- ✅ TypeScript lint kontrolü
|
||
|
||
## Notlar
|
||
|
||
- Resimler `site-assets` bucket'ında saklanır
|
||
- Eski resimler silinirken yeni resim yüklenirken otomatik olarak temizlenir
|
||
- Manuel URL girişi de desteklenir (harici resimler için)
|
||
- Tüm resimler public olarak erişilebilir
|
||
- Admin rolü olmayan kullanıcılar resim yükleyemez
|