first commit
This commit is contained in:
837
app/assistants/converters/images/page.tsx
Normal file
837
app/assistants/converters/images/page.tsx
Normal file
@@ -0,0 +1,837 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect, FormEvent, ChangeEvent } from "react";
|
||||
import { Images } from "@/Type/images";
|
||||
import PreloaderAndSearch from "@/components/PreloaderAndSearch";
|
||||
import Header from "@/components/Header";
|
||||
import CookieAlert from "@/components/CookieAlert";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
const MEDIA_BASE_URL = process.env.NEXT_PUBLIC_MEDIA_BASE_URL || "http://localhost:8000/media";
|
||||
|
||||
export default function ImageUploadPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
format: "avif",
|
||||
width: "",
|
||||
height: "",
|
||||
quality: "",
|
||||
});
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [uploadedImage, setUploadedImage] = useState<Images | null>(null);
|
||||
const [imagesList, setImagesList] = useState<Images[]>([]);
|
||||
const [loadingImages, setLoadingImages] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copiedUrl, setCopiedUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/auth/login");
|
||||
} else if (status === "authenticated") {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [status, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "authenticated" && session?.accessToken) {
|
||||
fetchImages();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status]);
|
||||
|
||||
const fetchImages = async () => {
|
||||
if (!session?.accessToken) {
|
||||
console.log("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingImages(true);
|
||||
try {
|
||||
console.log("Fetching images from:", `${API_BASE_URL}/images/list/`);
|
||||
const response = await fetch(`${API_BASE_URL}/images/list/`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Response status:", response.status);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log("Fetched images data:", data);
|
||||
// Eğer paginated response ise results'ı al, değilse direkt array
|
||||
const images = Array.isArray(data) ? data : (data.results || []);
|
||||
console.log("Processed images list:", images);
|
||||
setImagesList(images);
|
||||
} else if (response.status === 401) {
|
||||
console.error("Unauthorized - signing out");
|
||||
signOut({ callbackUrl: "/auth/login" });
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error("Failed to fetch images:", response.status, errorText);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching images:", err);
|
||||
} finally {
|
||||
setLoadingImages(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
setError("");
|
||||
|
||||
// Preview oluştur
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Format seçimi kullanıcı tarafından yapılacak, varsayılan avif
|
||||
// Dosya formatını sadece bilgi amaçlı tutuyoruz
|
||||
|
||||
// Resim boyutlarını al
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
width: img.width.toString(),
|
||||
height: img.height.toString(),
|
||||
}));
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSuccess(false);
|
||||
setUploadedImage(null);
|
||||
|
||||
if (!selectedFile) {
|
||||
setError("Lütfen bir resim dosyası seçin.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.title || formData.title.trim() === "") {
|
||||
setError("Başlık alanı zorunludur.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session?.accessToken) {
|
||||
setError("Oturum açmanız gerekiyor.");
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
|
||||
try {
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append("image", selectedFile);
|
||||
uploadFormData.append("title", formData.title.trim());
|
||||
uploadFormData.append("format", formData.format);
|
||||
|
||||
if (formData.width) {
|
||||
uploadFormData.append("width", formData.width);
|
||||
}
|
||||
if (formData.height) {
|
||||
uploadFormData.append("height", formData.height);
|
||||
}
|
||||
if (formData.quality) {
|
||||
uploadFormData.append("quality", formData.quality);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/images/upload/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
body: uploadFormData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUploadedImage(data);
|
||||
setSuccess(true);
|
||||
setSelectedFile(null);
|
||||
setPreview(null);
|
||||
setFormData({
|
||||
title: "",
|
||||
format: "avif",
|
||||
width: "",
|
||||
height: "",
|
||||
quality: "",
|
||||
});
|
||||
// Yeni yüklenen resmi listeye ekle
|
||||
fetchImages();
|
||||
setTimeout(() => setSuccess(false), 5000);
|
||||
} else if (response.status === 401) {
|
||||
signOut({ callbackUrl: "/auth/login" });
|
||||
} else {
|
||||
let errorMessage = "Resim yüklenirken bir hata oluştu.";
|
||||
|
||||
try {
|
||||
const contentType = response.headers.get("content-type");
|
||||
let errorData: any = {};
|
||||
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
errorData = await response.json();
|
||||
console.error("API Error Response:", errorData);
|
||||
} else {
|
||||
const text = await response.text();
|
||||
console.error("API Error Response (text):", text);
|
||||
try {
|
||||
errorData = JSON.parse(text);
|
||||
} catch {
|
||||
errorMessage = text || `Sunucu hatası (${response.status})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Farklı hata formatlarını kontrol et
|
||||
if (errorData.detail) {
|
||||
errorMessage = Array.isArray(errorData.detail)
|
||||
? errorData.detail.join(', ')
|
||||
: errorData.detail;
|
||||
} else if (errorData.message) {
|
||||
errorMessage = Array.isArray(errorData.message)
|
||||
? errorData.message.join(', ')
|
||||
: errorData.message;
|
||||
} else if (errorData.error) {
|
||||
errorMessage = Array.isArray(errorData.error)
|
||||
? errorData.error.join(', ')
|
||||
: errorData.error;
|
||||
} else if (typeof errorData === 'string') {
|
||||
errorMessage = errorData;
|
||||
} else {
|
||||
// Field-specific errors - Django REST Framework formatı
|
||||
const errorMessages: string[] = [];
|
||||
Object.keys(errorData).forEach(key => {
|
||||
const value = errorData[key];
|
||||
if (Array.isArray(value)) {
|
||||
errorMessages.push(`${key}: ${value.join(', ')}`);
|
||||
} else if (typeof value === 'string') {
|
||||
errorMessages.push(`${key}: ${value}`);
|
||||
} else if (typeof value === 'object') {
|
||||
errorMessages.push(`${key}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
});
|
||||
if (errorMessages.length > 0) {
|
||||
errorMessage = errorMessages.join(' | ');
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("Error parsing response:", parseError);
|
||||
errorMessage = `Sunucu hatası (${response.status}): ${response.statusText}`;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Upload error:", err);
|
||||
const errorMessage = err instanceof Error
|
||||
? err.message
|
||||
: "Bir hata oluştu. Lütfen tekrar deneyin.";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading" || loading) {
|
||||
return (
|
||||
<>
|
||||
<PreloaderAndSearch />
|
||||
<Header />
|
||||
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||
<div className="text-center">
|
||||
<div className="spinner-grow text-primary" role="status">
|
||||
<span className="visually-hidden">Yükleniyor...</span>
|
||||
</div>
|
||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreloaderAndSearch />
|
||||
<Header />
|
||||
|
||||
{/* Image Upload Section */}
|
||||
<div className="divider"></div>
|
||||
<section className="py-5 py-md-4 py-xl-5">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="section-heading text-center mb-5">
|
||||
<h2 className="mb-0">Resim Yükle</h2>
|
||||
<p className="mt-3">Resim dosyanızı yükleyin ve işleyin</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<i className="ti ti-alert-circle me-2"></i>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="alert alert-success" role="alert">
|
||||
<i className="ti ti-check me-2"></i>
|
||||
Resim başarıyla yüklendi!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card border-0 shadow-sm">
|
||||
<div className="card-body p-4 p-md-5">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* File Input */}
|
||||
<div className="mb-4">
|
||||
<label className="form-label fw-bold mb-3">
|
||||
Resim Dosyası <span className="text-danger">*</span>
|
||||
</label>
|
||||
<div className="border border-2 border-dashed rounded-4 p-5 text-center" style={{
|
||||
borderColor: preview ? '#601FEB' : 'rgba(31, 30, 33, 0.3)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
{preview ? (
|
||||
<div className="space-y-3">
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
className="img-fluid rounded-3 mx-auto d-block"
|
||||
style={{ maxHeight: '300px', objectFit: 'contain' }}
|
||||
/>
|
||||
<p className="text-muted mb-2">{selectedFile?.name}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedFile(null);
|
||||
setPreview(null);
|
||||
setFormData(prev => ({ ...prev, format: "avif", width: "", height: "" }));
|
||||
}}
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
<i className="ti ti-x me-1"></i> Kaldır
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<i className="ti ti-photo" style={{ fontSize: '3rem', color: '#601FEB' }}></i>
|
||||
</div>
|
||||
<label className="btn btn-primary cursor-pointer">
|
||||
<i className="ti ti-upload me-2"></i> Dosya Seç
|
||||
<input
|
||||
type="file"
|
||||
className="d-none"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<p className="text-muted mt-3 mb-0">PNG, JPG, GIF, WEBP (MAX. 10MB)</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="title" className="form-label fw-bold">
|
||||
Başlık <span className="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="Resim başlığı"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Format, Width, Height */}
|
||||
<div className="row g-4 mb-4">
|
||||
<div className="col-12 col-md-4">
|
||||
<label htmlFor="format" className="form-label fw-bold">
|
||||
Format <span className="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="format"
|
||||
name="format"
|
||||
value={formData.format}
|
||||
onChange={handleInputChange}
|
||||
className="form-select"
|
||||
required
|
||||
>
|
||||
<option value="avif">AVIF</option>
|
||||
<option value="webp">WEBP</option>
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpg">JPG</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<label htmlFor="width" className="form-label fw-bold">
|
||||
Genişlik (px)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="width"
|
||||
name="width"
|
||||
value={formData.width}
|
||||
onChange={handleInputChange}
|
||||
className="form-control bg-light"
|
||||
placeholder="Otomatik"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<label htmlFor="height" className="form-label fw-bold">
|
||||
Yükseklik (px)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="height"
|
||||
name="height"
|
||||
value={formData.height}
|
||||
onChange={handleInputChange}
|
||||
className="form-control bg-light"
|
||||
placeholder="Otomatik"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quality */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="quality" className="form-label fw-bold">
|
||||
Kalite (1-100)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="quality"
|
||||
name="quality"
|
||||
value={formData.quality}
|
||||
onChange={handleInputChange}
|
||||
min="1"
|
||||
max="100"
|
||||
className="form-control"
|
||||
placeholder="80 (varsayılan)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Buttons */}
|
||||
<div className="d-flex justify-content-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="btn btn-outline-secondary"
|
||||
disabled={uploading}
|
||||
>
|
||||
<i className="ti ti-x me-1"></i> İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={uploading || !selectedFile}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Yükleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="ti ti-upload me-1"></i> Yükle
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Uploaded Image Info */}
|
||||
{uploadedImage && (
|
||||
<div className="card border-0 shadow-sm mt-4">
|
||||
<div className="card-body p-4 p-md-5">
|
||||
<h3 className="h4 fw-bold mb-4">
|
||||
<i className="ti ti-check-circle text-success me-2"></i>
|
||||
Yüklenen Resim Bilgileri
|
||||
</h3>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>ID:</strong> <span className="text-muted">{uploadedImage.id}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>Başlık:</strong> <span className="text-muted">{uploadedImage.title || "N/A"}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>Format:</strong> <span className="text-muted">{uploadedImage.format}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>Boyut:</strong> <span className="text-muted">
|
||||
{uploadedImage.width} x {uploadedImage.height} px
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>Kalite:</strong> <span className="text-muted">{uploadedImage.quality}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-2">
|
||||
<strong>Dosya Boyutu:</strong> <span className="text-muted">
|
||||
{(uploadedImage.size / 1024).toFixed(2)} KB
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{uploadedImage.path && (
|
||||
<div className="col-12">
|
||||
<p className="mb-2">
|
||||
<strong>Yol:</strong>
|
||||
</p>
|
||||
<a
|
||||
href={uploadedImage.path}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary text-break"
|
||||
>
|
||||
{uploadedImage.path}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{uploadedImage.processed_path && (
|
||||
<div className="col-12">
|
||||
<p className="mb-2">
|
||||
<strong>İşlenmiş Yol:</strong>
|
||||
</p>
|
||||
<a
|
||||
href={uploadedImage.processed_path}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary text-break"
|
||||
>
|
||||
{uploadedImage.processed_path}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Images List */}
|
||||
<div className="card border-0 shadow-sm mt-4">
|
||||
<div className="card-body p-4 p-md-5">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 className="h4 fw-bold mb-0">
|
||||
<i className="ti ti-photo me-2"></i>
|
||||
Yüklenen Resimler
|
||||
</h3>
|
||||
<button
|
||||
onClick={fetchImages}
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
disabled={loadingImages}
|
||||
>
|
||||
<i className={`ti ti-refresh ${loadingImages ? 'spinner-border spinner-border-sm' : ''} me-1`}></i>
|
||||
Yenile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loadingImages ? (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Yükleniyor...</span>
|
||||
</div>
|
||||
<p className="mt-3 text-muted">Resimler yükleniyor...</p>
|
||||
</div>
|
||||
) : imagesList.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<i className="ti ti-photo-off" style={{ fontSize: '3rem', color: '#ccc' }}></i>
|
||||
<p className="mt-3 text-muted">Henüz resim yüklenmemiş.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{imagesList.map((image) => {
|
||||
// Path'leri tam URL'ye çevir
|
||||
const getImageUrl = (path: string | null | undefined) => {
|
||||
if (!path) return null;
|
||||
// Eğer zaten tam URL ise (http ile başlıyorsa) direkt dön
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
}
|
||||
// Eğer path sadece dosya adı ise (slash içermiyorsa), processed klasörü altında olabilir
|
||||
if (!path.includes('/')) {
|
||||
return `${MEDIA_BASE_URL}/processed/${path}`;
|
||||
}
|
||||
// Relative path ise media base URL ile birleştir
|
||||
// Path zaten processed/ ile başlıyorsa direkt birleştir
|
||||
return `${MEDIA_BASE_URL}/${path}`;
|
||||
};
|
||||
|
||||
// path değeri genellikle processed/ ile başlar ve daha güvenilirdir
|
||||
// processed_path ise bazen sadece dosya adı olabilir
|
||||
const originalUrl = getImageUrl(image.path);
|
||||
const processedUrl = getImageUrl(image.processed_path);
|
||||
// Önce path'i kullan, yoksa processed_path'i dene
|
||||
const imageUrl = originalUrl || processedUrl;
|
||||
|
||||
console.log("Rendering image:", image.id, "path:", image.path, "processed_path:", image.processed_path, "Final URL:", imageUrl);
|
||||
|
||||
return (
|
||||
<div key={image.id} className="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<div className="card border h-100">
|
||||
{imageUrl ? (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={image.title || `Image ${image.id}`}
|
||||
className="card-img-top"
|
||||
style={{
|
||||
height: '200px',
|
||||
width: '100%',
|
||||
objectFit: 'cover',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => window.open(imageUrl, '_blank')}
|
||||
onError={(e) => {
|
||||
console.error("Image load error for:", imageUrl, "Trying fallback...");
|
||||
// Eğer ilk URL çalışmazsa, diğer path'i dene
|
||||
const fallbackUrl = originalUrl ? processedUrl : originalUrl;
|
||||
if (fallbackUrl && fallbackUrl !== imageUrl) {
|
||||
e.currentTarget.src = fallbackUrl;
|
||||
} else {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="card-img-top d-flex align-items-center justify-content-center bg-light"
|
||||
style={{ height: '200px' }}
|
||||
>
|
||||
<i className="ti ti-photo" style={{ fontSize: '3rem', color: '#ccc' }}></i>
|
||||
</div>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title mb-2" style={{ fontSize: '0.9rem' }}>
|
||||
{image.title || 'Başlıksız'}
|
||||
</h5>
|
||||
<div className="small text-muted mb-2">
|
||||
<div>
|
||||
<i className="ti ti-ruler me-1"></i>
|
||||
{image.width && image.height ? `${image.width} x ${image.height} px` : 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<i className="ti ti-file me-1"></i>
|
||||
{image.format ? image.format.toUpperCase() : 'N/A'}
|
||||
</div>
|
||||
<div>
|
||||
<i className="ti ti-database me-1"></i>
|
||||
{image.size ? `${(image.size / 1024).toFixed(2)} KB` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-2 mt-3 flex-wrap">
|
||||
{originalUrl && (
|
||||
<a
|
||||
href={originalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
title="Orijinal Resim"
|
||||
>
|
||||
<i className="ti ti-external-link"></i>
|
||||
</a>
|
||||
)}
|
||||
{processedUrl && (
|
||||
<a
|
||||
href={processedUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-sm btn-outline-success"
|
||||
title="İşlenmiş Resim"
|
||||
>
|
||||
<i className="ti ti-check"></i>
|
||||
</a>
|
||||
)}
|
||||
{imageUrl && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(imageUrl);
|
||||
setCopiedUrl(imageUrl);
|
||||
setTimeout(() => setCopiedUrl(null), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy URL:", err);
|
||||
// Fallback: Eski tarayıcılar için
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = imageUrl;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.opacity = '0';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
setCopiedUrl(imageUrl);
|
||||
setTimeout(() => setCopiedUrl(null), 2000);
|
||||
} catch (fallbackErr) {
|
||||
console.error("Fallback copy failed:", fallbackErr);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}}
|
||||
className={`btn btn-sm ${copiedUrl === imageUrl ? 'btn-success' : 'btn-outline-secondary'}`}
|
||||
title="URL'yi Kopyala"
|
||||
>
|
||||
<i className={`ti ${copiedUrl === imageUrl ? 'ti-check' : 'ti-copy'}`}></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-footer bg-transparent border-top">
|
||||
<small className="text-muted">
|
||||
<i className="ti ti-calendar me-1"></i>
|
||||
{image.created_at ? new Date(image.created_at).toLocaleDateString('tr-TR') : 'N/A'}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="divider"></div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="footer-wrapper">
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="row g-5">
|
||||
<div className="col-12 col-sm-6 col-lg-4">
|
||||
<div className="footer-card pe-lg-5">
|
||||
<Link href="/" className="d-block mb-4">
|
||||
<img src="/assets/img/core-img/logo.png" alt="" />
|
||||
</Link>
|
||||
<p className="mb-0">Complete authentication solution built with Django REST API and Next.js.</p>
|
||||
<div className="social-nav">
|
||||
<a href="#"><i className="ti ti-brand-facebook"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-x"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-linkedin"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-instagram"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-lg">
|
||||
<div className="footer-card">
|
||||
<h5 className="mb-4">Quick Links</h5>
|
||||
<ul className="footer-nav">
|
||||
<li><Link href="/">Home</Link></li>
|
||||
<li><Link href="/auth/register">Register</Link></li>
|
||||
<li><Link href="/auth/login">Login</Link></li>
|
||||
{session && <li><Link href="/dashboard">Dashboard</Link></li>}
|
||||
{session && <li><Link href="/profile">Profile</Link></li>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-lg">
|
||||
<div className="footer-card">
|
||||
<h5 className="mb-4">Features</h5>
|
||||
<ul className="footer-nav">
|
||||
<li><a href="#">Email Verification</a></li>
|
||||
<li><a href="#">Social Login</a></li>
|
||||
<li><a href="#">Password Reset</a></li>
|
||||
<li><a href="#">User Profile</a></li>
|
||||
<li><a href="#">Token Management</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-lg">
|
||||
<div className="footer-card">
|
||||
<h5 className="mb-4">Resources</h5>
|
||||
<ul className="footer-nav">
|
||||
<li><a href="/AUTH.md">API Documentation</a></li>
|
||||
<li><a href="/SETUP.md">Setup Guide</a></li>
|
||||
<li><a href="/ROUTES.md">Routes</a></li>
|
||||
<li><a href="#">FAQs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
<div className="copyright-wrapper">
|
||||
<div className="container">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-12 col-md-6">
|
||||
<p className="mb-3 mb-md-0 copyright">
|
||||
Copyright © <span>{new Date().getFullYear()}</span> <a href="#">Your Company</a> All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="footer-bottom-nav">
|
||||
<a href="#">Privacy & Terms</a>
|
||||
<a href="#">FAQ</a>
|
||||
<a href="#">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Cookie Alert */}
|
||||
<CookieAlert />
|
||||
|
||||
{/* Scroll To Top */}
|
||||
<button id="scrollTopButton" className="softora-scrolltop scrolltop-hide">
|
||||
<i className="ti ti-chevron-up"></i>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user