first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:12:36 +03:00
commit e881f38e4e
278 changed files with 24095 additions and 0 deletions

View 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>
</>
);
}

View File

@@ -0,0 +1,167 @@
import { useState } from 'react';
import Swal from 'sweetalert2';
export interface UseJsonToTypeReturn {
jsonInput: string;
setJsonInput: (value: string) => void;
typeOutput: string;
setTypeOutput: (value: string) => void;
typeName: string;
setTypeName: (value: string) => void;
error: string;
setError: (value: string) => void;
copied: boolean;
setCopied: (value: boolean) => void;
convertJsonToType: () => void;
handleCopy: () => void;
handleClear: () => void;
loadExample: () => void;
}
const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
const getTypeFromValue = (value: unknown, key?: string): string => {
if (value === null) return 'null';
if (Array.isArray(value)) {
if (value.length === 0) return 'any[]';
const firstItem = value[0];
const itemType = getTypeFromValue(firstItem);
return `${itemType}[]`;
}
const type = typeof value;
if (type === 'object') {
return generateTypeDefinition(value, key ? capitalize(key) : 'NestedType', false);
}
return type;
};
const generateTypeDefinition = (obj: unknown, name: string, isRoot: boolean = true): string => {
if (typeof obj !== 'object' || obj === null) {
return typeof obj;
}
const entries = Object.entries(obj);
const properties = entries.map(([key, value]) => {
const typeStr = getTypeFromValue(value, key);
return ` ${key}: ${typeStr};`;
}).join('\n');
return `${isRoot ? 'export ' : ''}interface ${name} {\n${properties}\n}`;
};
const exampleJson = `{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"isActive": true,
"roles": ["admin", "user"],
"address": {
"street": "123 Main St",
"city": "New York",
"zipCode": "10001"
},
"tags": ["developer", "designer"]
}`;
export const useJsonToType = (): UseJsonToTypeReturn => {
const [jsonInput, setJsonInput] = useState('');
const [typeOutput, setTypeOutput] = useState('');
const [error, setError] = useState('');
const [typeName, setTypeName] = useState('MyType');
const [copied, setCopied] = useState(false);
const convertJsonToType = () => {
try {
setError('');
setCopied(false);
if (!jsonInput.trim()) {
Swal.fire({
icon: 'warning',
title: 'JSON Eksik',
text: 'Lütfen bir JSON girin',
confirmButtonText: 'Tamam',
});
setError('Lütfen bir JSON girin');
return;
}
const parsed = JSON.parse(jsonInput);
const result = generateTypeDefinition(parsed, typeName || 'MyType');
setTypeOutput(result);
// Başarılı dönüştürme mesajı
Swal.fire({
icon: 'success',
title: 'Dönüştürme Başarılı!',
text: 'TypeScript interface oluşturuldu',
timer: 1500,
showConfirmButton: false,
});
} catch (err) {
const errorMsg = 'Geçersiz JSON formatı: ' + (err as Error).message;
setError(errorMsg);
setTypeOutput('');
Swal.fire({
icon: 'error',
title: 'JSON Hatası',
text: errorMsg,
confirmButtonText: 'Tamam',
});
}
};
const handleCopy = () => {
navigator.clipboard.writeText(typeOutput);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
// Toast mesajı
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Kopyalandı!',
showConfirmButton: false,
timer: 1500,
});
};
const handleClear = () => {
setJsonInput('');
setTypeOutput('');
setError('');
setTypeName('MyType');
setCopied(false);
};
const loadExample = () => {
setJsonInput(exampleJson);
setError('');
setCopied(false);
};
return {
jsonInput,
setJsonInput,
typeOutput,
setTypeOutput,
typeName,
setTypeName,
error,
setError,
copied,
setCopied,
convertJsonToType,
handleCopy,
handleClear,
loadExample,
};
};

View File

@@ -0,0 +1,153 @@
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import Swal from 'sweetalert2';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api/v1';
export interface UseJsonToTypeApiReturn {
title: string;
setTitle: (value: string) => void;
saveSuccess: string;
saveError: string;
isSaving: boolean;
handleSave: (jsonData: string, typeData: string) => Promise<void>;
clearMessages: () => void;
}
export const useJsonToTypeApi = (): UseJsonToTypeApiReturn => {
const { data: session, status } = useSession();
const router = useRouter();
const [title, setTitle] = useState('');
const [saveSuccess, setSaveSuccess] = useState('');
const [saveError, setSaveError] = useState('');
const [isSaving, setIsSaving] = useState(false);
const clearMessages = () => {
setSaveSuccess('');
setSaveError('');
};
const handleSave = async (jsonData: string, typeData: string) => {
// Login kontrolü
if (status !== 'authenticated' || !session) {
Swal.fire({
icon: 'warning',
title: 'Giriş Gerekli',
text: 'Kaydetmek için giriş yapmalısınız!',
confirmButtonText: 'Giriş Yap',
showCancelButton: true,
cancelButtonText: 'İptal',
}).then((result) => {
if (result.isConfirmed) {
router.push('/auth/login');
}
});
return;
}
// Validation
if (!title.trim()) {
Swal.fire({
icon: 'error',
title: 'Başlık Eksik',
text: 'Lütfen bir başlık girin',
confirmButtonText: 'Tamam',
});
return;
}
if (!jsonData.trim()) {
Swal.fire({
icon: 'error',
title: 'JSON Eksik',
text: 'Lütfen JSON verisi girin',
confirmButtonText: 'Tamam',
});
return;
}
if (!typeData.trim()) {
Swal.fire({
icon: 'error',
title: 'Dönüştürme Gerekli',
text: 'Lütfen önce JSON\'u TypeScript\'e dönüştürün',
confirmButtonText: 'Tamam',
});
return;
}
setIsSaving(true);
setSaveError('');
setSaveSuccess('');
try {
const response = await fetch(`${API_BASE_URL}/utils/jasontotype/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.accessToken}`,
},
body: JSON.stringify({
title: title.trim(),
json_data: jsonData.trim(),
type_data: typeData.trim(),
}),
});
const data = await response.json();
if (!response.ok) {
// Unique constraint hatası kontrolü
if (data.json_data || data.type_data) {
const errors = [];
if (data.json_data) errors.push('Bu JSON verisi zaten kayıtlı');
if (data.type_data) errors.push('Bu TypeScript tipi zaten kayıtlı');
Swal.fire({
icon: 'error',
title: 'Kayıt Mevcut',
text: errors.join(' ve '),
confirmButtonText: 'Tamam',
});
return;
}
throw new Error(data.detail || data.error || 'Kaydetme başarısız');
}
// Başarılı kayıt
Swal.fire({
icon: 'success',
title: 'Başarılı!',
text: 'Dönüştürme başarıyla kaydedildi',
timer: 2000,
showConfirmButton: false,
});
setSaveSuccess('Başarıyla kaydedildi!');
setTimeout(() => setSaveSuccess(''), 3000);
} catch (err) {
Swal.fire({
icon: 'error',
title: 'Hata',
text: (err as Error).message,
confirmButtonText: 'Tamam',
});
setSaveError((err as Error).message);
} finally {
setIsSaving(false);
}
};
return {
title,
setTitle,
saveSuccess,
saveError,
isSaving,
handleSave,
clearMessages,
};
};

View File

@@ -0,0 +1,201 @@
import { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import Swal from 'sweetalert2';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api/v1';
export interface JsonToTypeItem {
id: number;
title: string;
json_data: string;
type_data: string;
created_at: string;
updated_at: string;
}
export interface UseJsonToTypeListReturn {
items: JsonToTypeItem[];
isLoading: boolean;
error: string;
fetchItems: () => Promise<void>;
loadItem: (item: JsonToTypeItem) => void;
downloadJson: (item: JsonToTypeItem) => void;
downloadType: (item: JsonToTypeItem) => void;
deleteItem: (id: number) => Promise<void>;
}
export const useJsonToTypeList = (
onLoad?: (jsonData: string, typeData: string, title: string) => void
): UseJsonToTypeListReturn => {
const { data: session, status } = useSession();
const [items, setItems] = useState<JsonToTypeItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const fetchItems = async () => {
if (status !== 'authenticated' || !session) {
console.log('Kullanıcı authenticated değil');
return;
}
setIsLoading(true);
setError('');
try {
console.log('API çağrısı yapılıyor:', `${API_BASE_URL}/utils/jasontotype/`);
const response = await fetch(`${API_BASE_URL}/utils/jasontotype/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.accessToken}`,
},
});
console.log('Response status:', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error('API Error:', errorData);
throw new Error(errorData.detail || 'Veriler yüklenirken hata oluştu');
}
const data = await response.json();
console.log('Gelen kayıt sayısı:', data.length);
console.log('Kayıtlar:', data);
setItems(Array.isArray(data) ? data : []);
} catch (err) {
console.error('Fetch error:', err);
setError((err as Error).message);
Swal.fire({
icon: 'error',
title: 'Hata',
text: (err as Error).message,
confirmButtonText: 'Tamam',
});
} finally {
setIsLoading(false);
}
};
// İlk yüklemede kayıtları getir
useEffect(() => {
if (status === 'authenticated') {
fetchItems();
}
}, [status]);
const loadItem = (item: JsonToTypeItem) => {
if (onLoad) {
onLoad(item.json_data, item.type_data, item.title);
}
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'Kayıt yüklendi!',
showConfirmButton: false,
timer: 1500,
});
};
const downloadJson = (item: JsonToTypeItem) => {
const blob = new Blob([item.json_data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${item.title.replace(/\s+/g, '_')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'JSON indirildi!',
showConfirmButton: false,
timer: 1500,
});
};
const downloadType = (item: JsonToTypeItem) => {
const blob = new Blob([item.type_data], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${item.title.replace(/\s+/g, '_')}.ts`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: 'TypeScript indirildi!',
showConfirmButton: false,
timer: 1500,
});
};
const deleteItem = async (id: number) => {
const result = await Swal.fire({
icon: 'warning',
title: 'Emin misiniz?',
text: 'Bu kaydı silmek istediğinizden emin misiniz?',
showCancelButton: true,
confirmButtonText: 'Evet, Sil',
cancelButtonText: 'İptal',
confirmButtonColor: '#d33',
});
if (!result.isConfirmed) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/utils/jasontotype/${id}/`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${session?.accessToken}`,
},
});
if (!response.ok) {
throw new Error('Silme işlemi başarısız');
}
setItems(items.filter(item => item.id !== id));
Swal.fire({
icon: 'success',
title: 'Silindi!',
text: 'Kayıt başarıyla silindi',
timer: 1500,
showConfirmButton: false,
});
} catch (err) {
Swal.fire({
icon: 'error',
title: 'Hata',
text: (err as Error).message,
confirmButtonText: 'Tamam',
});
}
};
return {
items,
isLoading,
error,
fetchItems,
loadItem,
downloadJson,
downloadType,
deleteItem,
};
};

View File

@@ -0,0 +1,430 @@
'use client';
import React from 'react';
import { useSession } from 'next-auth/react';
//import 'bootstrap/dist/css/bootstrap.min.css';
import { useJsonToType } from './hooks/useJsonToType';
import { useJsonToTypeApi } from './hooks/useJsonToTypeApi';
import { useJsonToTypeList } from './hooks/useJsonToTypeList';
import PreloaderAndSearch from "@/components/PreloaderAndSearch";
import Header from "@/components/Header";
export default function JsonToTypePage() {
const { status } = useSession();
// Converter logic
const {
jsonInput,
setJsonInput,
typeOutput,
setTypeOutput,
typeName,
setTypeName,
error,
copied,
convertJsonToType,
handleCopy,
handleClear: clearConverter,
loadExample,
} = useJsonToType();
// API logic
const {
title,
setTitle,
saveSuccess,
saveError,
isSaving,
handleSave: saveToApi,
clearMessages,
} = useJsonToTypeApi();
// List logic
const {
items,
isLoading,
fetchItems,
loadItem,
downloadJson,
downloadType,
deleteItem,
} = useJsonToTypeList((jsonData, typeData, itemTitle) => {
setJsonInput(jsonData);
setTypeOutput(typeData);
setTitle(itemTitle);
});
const handleClear = () => {
clearConverter();
setTitle('');
clearMessages();
};
const handleSave = async () => {
await saveToApi(jsonInput, typeOutput);
// Kayıt başarılıysa listeyi yenile
fetchItems();
};
return (
<>
<PreloaderAndSearch />
<Header />
{/* Spacer for fixed header */}
<div style={{ height: '150px' }}></div>
<div className="min-vh-100 bg-light py-5">
<div className="container">
<div className="row mb-4">
<div className="col-12">
<div className="text-center">
<h1 className="display-4 fw-bold text-primary mb-3">
<i className="bi bi-code-square me-3"></i>
JSON to TypeScript Converter
</h1>
<p className="lead text-muted">
JSON verilerinizi TypeScript interface tanımlarına kolayca dönüştürün
</p>
</div>
</div>
</div>
<div className="row g-4">
{/* Input Section */}
<div className="col-lg-6">
<div className="card shadow-sm border-0 h-100">
<div className="card-header bg-primary text-white py-3">
<h5 className="mb-0">
<i className="bi bi-file-earmark-code me-2"></i>
JSON Input
</h5>
</div>
<div className="card-body">
<div className="mb-3">
<label htmlFor="title" className="form-label fw-semibold">
<i className="bi bi-bookmark me-1"></i>
Başlık:
</label>
<input
type="text"
className="form-control"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Örn: User API Response Type"
/>
<small className="text-muted">Kaydetmek için bir başlık girin</small>
</div>
<div className="mb-3">
<label htmlFor="typeName" className="form-label fw-semibold">
Interface Adı:
</label>
<input
type="text"
className="form-control"
id="typeName"
value={typeName}
onChange={(e) => setTypeName(e.target.value)}
placeholder="MyType"
/>
</div>
<div className="mb-3">
<label htmlFor="jsonInput" className="form-label fw-semibold">
JSON Veriniz:
</label>
<textarea
className="form-control font-monospace"
id="jsonInput"
rows={15}
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
placeholder="JSON kodunuzu buraya yapıştırın..."
style={{ fontSize: '0.9rem' }}
/>
</div>
{error && (
<div className="alert alert-danger d-flex align-items-center" role="alert">
<i className="bi bi-exclamation-triangle-fill me-2"></i>
<div>{error}</div>
</div>
)}
<div className="d-flex gap-2 flex-wrap">
<button
className="btn btn-primary btn-lg"
onClick={convertJsonToType}
>
<i className="bi bi-arrow-right-circle me-2"></i>
Dönüştür
</button>
<button
className="btn btn-outline-secondary"
onClick={loadExample}
>
<i className="bi bi-file-earmark-text me-2"></i>
Örnek Yükle
</button>
<button
className="btn btn-outline-danger"
onClick={handleClear}
>
<i className="bi bi-trash me-2"></i>
Temizle
</button>
</div>
</div>
</div>
</div>
{/* Output Section */}
<div className="col-lg-6">
<div className="card shadow-sm border-0 h-100">
<div className="card-header bg-success text-white py-3">
<h5 className="mb-0">
<i className="bi bi-file-earmark-code-fill me-2"></i>
TypeScript Output
</h5>
</div>
<div className="card-body">
{typeOutput ? (
<>
<div className="mb-3">
<div className="d-flex justify-content-between align-items-center mb-2">
<label className="form-label fw-semibold mb-0">
TypeScript Interface:
</label>
<button
className={`btn btn-sm ${copied ? 'btn-success' : 'btn-outline-primary'}`}
onClick={handleCopy}
>
<i className={`bi ${copied ? 'bi-check-circle-fill' : 'bi-clipboard'} me-1`}></i>
{copied ? 'Kopyalandı!' : 'Kopyala'}
</button>
</div>
<pre className="bg-dark text-light p-3 rounded" style={{ maxHeight: '400px', overflow: 'auto' }}>
<code>{typeOutput}</code>
</pre>
</div>
{/* Save Success/Error Messages */}
{saveSuccess && (
<div className="alert alert-success d-flex align-items-center mb-3" role="alert">
<i className="bi bi-check-circle-fill me-2"></i>
<div>{saveSuccess}</div>
</div>
)}
{saveError && (
<div className="alert alert-danger d-flex align-items-center mb-3" role="alert">
<i className="bi bi-exclamation-triangle-fill me-2"></i>
<div>{saveError}</div>
</div>
)}
{/* Save Button */}
<div className="d-grid gap-2">
<button
className="btn btn-success btn-lg"
onClick={handleSave}
disabled={isSaving || !title.trim()}
>
{isSaving ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
Kaydediliyor...
</>
) : (
<>
<i className="bi bi-save me-2"></i>
Kaydet
</>
)}
</button>
{status !== 'authenticated' && (
<small className="text-danger text-center">
<i className="bi bi-lock me-1"></i>
Kaydetmek için giriş yapmalısınız
</small>
)}
{!title.trim() && typeOutput && (
<small className="text-warning text-center">
<i className="bi bi-exclamation-circle me-1"></i>
Kaydetmek için başlık girmelisiniz
</small>
)}
</div>
</>
) : (
<div className="text-center text-muted py-5">
<i className="bi bi-arrow-left-circle display-1 mb-3"></i>
<p className="lead">
Dönüştürülmüş TypeScript kodunuz burada görünecek
</p>
</div>
)}
</div>
</div>
</div>
</div>
{/* Features Section */}
<div className="row mt-5">
<div className="col-12">
<div className="card shadow-sm border-0">
<div className="card-body">
<h5 className="card-title mb-3">
<i className="bi bi-info-circle me-2"></i>
Özellikler
</h5>
<div className="row">
<div className="col-md-4 mb-3">
<div className="d-flex">
<i className="bi bi-check-circle-fill text-success me-2"></i>
<div>
<strong>Otomatik Tip Algılama</strong>
<p className="text-muted small mb-0">String, number, boolean, array ve nested object desteklenir</p>
</div>
</div>
</div>
<div className="col-md-4 mb-3">
<div className="d-flex">
<i className="bi bi-check-circle-fill text-success me-2"></i>
<div>
<strong>İç İçe Objeler</strong>
<p className="text-muted small mb-0">Karmaşık JSON yapılarını otomatik olarak interface'lere dönüştürür</p>
</div>
</div>
</div>
<div className="col-md-4 mb-3">
<div className="d-flex">
<i className="bi bi-check-circle-fill text-success me-2"></i>
<div>
<strong>Hızlı Kopyalama</strong>
<p className="text-muted small mb-0">Tek tıkla oluşturulan kodu kopyalayın</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Saved Items Section */}
{status === 'authenticated' && (
<div className="row mt-5">
<div className="col-12">
<div className="card shadow-sm border-0">
<div className="card-header bg-info text-white py-3">
<div className="d-flex justify-content-between align-items-center">
<h5 className="mb-0">
<i className="bi bi-folder me-2"></i>
Kayıtlı Dönüşümlerim ({items.length})
</h5>
<button
className="btn btn-light btn-sm"
onClick={fetchItems}
disabled={isLoading}
>
<i className="bi bi-arrow-clockwise me-1"></i>
Yenile
</button>
</div>
</div>
<div className="card-body">
{isLoading ? (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Yükleniyor...</span>
</div>
</div>
) : items.length === 0 ? (
<div className="text-center text-muted py-5">
<i className="bi bi-inbox display-1 mb-3"></i>
<p className="lead">Henüz kaydedilmiş dönüşüm yok</p>
<p className="small">Dönüşümlerinizi kaydedin ve buradan kolayca erişin</p>
</div>
) : (
<div className="table-responsive">
<table className="table table-hover">
<thead>
<tr>
<th>Başlık</th>
<th>Tarih</th>
<th className="text-end">İşlemler</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id}>
<td>
<strong>{item.title}</strong>
</td>
<td>
<small className="text-muted">
{new Date(item.created_at).toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</small>
</td>
<td>
<div className="d-flex gap-2 justify-content-end">
<button
className="btn btn-sm btn-primary"
onClick={() => loadItem(item)}
title="Yükle"
>
<i className="bi bi-upload me-1"></i>
Yükle
</button>
<button
className="btn btn-sm btn-success"
onClick={() => downloadJson(item)}
title="JSON İndir"
>
<i className="bi bi-filetype-json me-1"></i>
JSON
</button>
<button
className="btn btn-sm btn-info text-white"
onClick={() => downloadType(item)}
title="TypeScript İndir"
>
<i className="bi bi-filetype-tsx me-1"></i>
TS
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => deleteItem(item.id)}
title="Sil"
>
<i className="bi bi-trash me-1"></i>
Sil
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
</div>
</>
);
}