Files
next-dj/app/assistants/converters/images/page.tsx
Beyhan Oğur e881f38e4e first commit
2026-04-26 22:12:36 +03:00

838 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
</>
);
}