first commit
This commit is contained in:
170
app/activate/[uid]/[token]/page.tsx
Normal file
170
app/activate/[uid]/[token]/page.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function ActivatePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const activateAccount = async () => {
|
||||
const { uid, token } = params;
|
||||
|
||||
if (!uid || !token) {
|
||||
setError("Geçersiz aktivasyon linki.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/activation/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uid,
|
||||
token,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push("/auth/login");
|
||||
}, 3000);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(
|
||||
data.detail ||
|
||||
"Aktivasyon başarısız. Link geçersiz veya süresi dolmuş olabilir."
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen daha sonra tekrar deneyin.");
|
||||
console.error("Activation error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
activateAccount();
|
||||
}, [params, router]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
{loading && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100">
|
||||
<svg
|
||||
className="animate-spin h-6 w-6 text-blue-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Hesabınız Aktifleştiriliyor...
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Lütfen bekleyin.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Aktivasyon Başarılı!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Hesabınız başarıyla aktifleştirildi. Artık giriş yapabilirsiniz.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-500">
|
||||
Giriş sayfasına yönlendiriliyorsunuz...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Aktivasyon Başarısız
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
<Link
|
||||
href="/auth/resend-activation"
|
||||
className="block w-full text-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Aktivasyon Emaili Tekrar Gönder
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="block w-full text-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Giriş Sayfasına Dön
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
219
app/api/auth/[...nextauth]/route.ts
Normal file
219
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import GithubProvider from "next-auth/providers/github";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { JWT } from "next-auth/jwt";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
// Django REST API - Token Refresh
|
||||
async function refreshAccessToken(token: JWT) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/auth/jwt/refresh/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refresh: token.refreshToken,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw data;
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: data.access,
|
||||
refreshToken: data.refresh ?? token.refreshToken,
|
||||
// Django JWT access token: 60 dakika (3600000 ms)
|
||||
accessTokenExpiry: Date.now() + 3600000,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error refreshing access token:", error);
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Django REST API - Social Auth Handler
|
||||
async function handleSocialAuth(provider: string, accessToken: string) {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/auth/social/${provider}/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Social authentication failed");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
GithubProvider({
|
||||
clientId: process.env.GITHUB_ID || "",
|
||||
clientSecret: process.env.GITHUB_SECRET || "",
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID || "",
|
||||
clientSecret: process.env.GOOGLE_SECRET || "",
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "Django REST API",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Django REST API - Login
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/auth/jwt/create/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data?.access && data?.refresh) {
|
||||
// Get user profile
|
||||
const userResponse = await fetch(
|
||||
`${API_BASE_URL}/auth/users/me/`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.access}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const userData = await userResponse.json();
|
||||
|
||||
return {
|
||||
id: userData.id?.toString() || credentials.email,
|
||||
email: userData.email,
|
||||
name: `${userData.first_name || ""} ${userData.last_name || ""}`.trim(),
|
||||
accessToken: data.access,
|
||||
refreshToken: data.refresh,
|
||||
accessTokenExpiry: Date.now() + 3600000, // 60 dakika
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Authentication error:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
// İlk login - Email/Password (Credentials Provider)
|
||||
if (user && account?.provider === "credentials") {
|
||||
token.accessToken = user.accessToken;
|
||||
token.refreshToken = user.refreshToken;
|
||||
token.accessTokenExpiry = user.accessTokenExpiry;
|
||||
return token;
|
||||
}
|
||||
|
||||
// İlk login - Social Auth (Google/GitHub)
|
||||
if (account && (account.provider === "google" || account.provider === "github")) {
|
||||
try {
|
||||
const providerMap: Record<string, string> = {
|
||||
google: "google-oauth2",
|
||||
github: "github",
|
||||
};
|
||||
|
||||
const djangoProvider = providerMap[account.provider];
|
||||
const socialData = await handleSocialAuth(
|
||||
djangoProvider,
|
||||
account.access_token!
|
||||
);
|
||||
|
||||
token.accessToken = socialData.access;
|
||||
token.refreshToken = socialData.refresh;
|
||||
token.accessTokenExpiry = Date.now() + 3600000;
|
||||
token.email = socialData.user.email;
|
||||
token.name = `${socialData.user.first_name || ""} ${socialData.user.last_name || ""}`.trim();
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error("Social auth error:", error);
|
||||
token.error = "SocialAuthError";
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
// Token hala geçerliyse mevcut tokeni döndür
|
||||
if (token.accessTokenExpiry && Date.now() < (token.accessTokenExpiry as number)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token süresi dolmuşsa refresh et
|
||||
return refreshAccessToken(token);
|
||||
},
|
||||
async session({ session, token }) {
|
||||
session.accessToken = token.accessToken as string;
|
||||
session.refreshToken = token.refreshToken as string;
|
||||
session.error = token.error as string | undefined;
|
||||
|
||||
if (token.email) {
|
||||
session.user = {
|
||||
...session.user,
|
||||
email: token.email as string,
|
||||
name: token.name as string,
|
||||
};
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
error: "/auth/error",
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 gün (refresh token süresi)
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
167
app/assistants/converters/jsontotype/hooks/useJsonToType.ts
Normal file
167
app/assistants/converters/jsontotype/hooks/useJsonToType.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
153
app/assistants/converters/jsontotype/hooks/useJsonToTypeApi.ts
Normal file
153
app/assistants/converters/jsontotype/hooks/useJsonToTypeApi.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
201
app/assistants/converters/jsontotype/hooks/useJsonToTypeList.ts
Normal file
201
app/assistants/converters/jsontotype/hooks/useJsonToTypeList.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
430
app/assistants/converters/jsontotype/page.tsx
Normal file
430
app/assistants/converters/jsontotype/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
170
app/auth/activate/[uid]/[token]/page.tsx
Normal file
170
app/auth/activate/[uid]/[token]/page.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function ActivatePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const activateAccount = async () => {
|
||||
const { uid, token } = params;
|
||||
|
||||
if (!uid || !token) {
|
||||
setError("Geçersiz aktivasyon linki.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/activation/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uid,
|
||||
token,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push("/auth/login");
|
||||
}, 3000);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(
|
||||
data.detail ||
|
||||
"Aktivasyon başarısız. Link geçersiz veya süresi dolmuş olabilir."
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen daha sonra tekrar deneyin.");
|
||||
console.error("Activation error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
activateAccount();
|
||||
}, [params, router]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
{loading && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100">
|
||||
<svg
|
||||
className="animate-spin h-6 w-6 text-blue-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Hesabınız Aktifleştiriliyor...
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Lütfen bekleyin.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Aktivasyon Başarılı!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Hesabınız başarıyla aktifleştirildi. Artık giriş yapabilirsiniz.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-500">
|
||||
Giriş sayfasına yönlendiriliyorsunuz...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Aktivasyon Başarısız
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
<Link
|
||||
href="/auth/resend-activation"
|
||||
className="block w-full text-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Aktivasyon Emaili Tekrar Gönder
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="block w-full text-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Giriş Sayfasına Dön
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
137
app/auth/error/page.tsx
Normal file
137
app/auth/error/page.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
|
||||
function ErrorContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const error = searchParams.get("error");
|
||||
|
||||
const errorMessages: Record<string, { title: string; description: string }> = {
|
||||
Configuration: {
|
||||
title: "Yapılandırma Hatası",
|
||||
description: "Authentication sisteminde bir yapılandırma hatası oluştu.",
|
||||
},
|
||||
AccessDenied: {
|
||||
title: "Erişim Reddedildi",
|
||||
description: "Bu kaynağa erişim izniniz yok.",
|
||||
},
|
||||
Verification: {
|
||||
title: "Doğrulama Hatası",
|
||||
description: "Doğrulama linki geçersiz veya süresi dolmuş.",
|
||||
},
|
||||
OAuthSignin: {
|
||||
title: "OAuth Giriş Hatası",
|
||||
description: "OAuth sağlayıcısına bağlanırken bir hata oluştu.",
|
||||
},
|
||||
OAuthCallback: {
|
||||
title: "OAuth Callback Hatası",
|
||||
description: "OAuth callback işlemi başarısız oldu.",
|
||||
},
|
||||
OAuthCreateAccount: {
|
||||
title: "Hesap Oluşturma Hatası",
|
||||
description: "OAuth ile hesap oluşturulurken bir hata oluştu.",
|
||||
},
|
||||
EmailCreateAccount: {
|
||||
title: "Email Hesap Oluşturma Hatası",
|
||||
description: "Email ile hesap oluşturulurken bir hata oluştu.",
|
||||
},
|
||||
Callback: {
|
||||
title: "Callback Hatası",
|
||||
description: "Authentication callback işlemi başarısız oldu.",
|
||||
},
|
||||
OAuthAccountNotLinked: {
|
||||
title: "Hesap Bağlantısı Hatası",
|
||||
description:
|
||||
"Bu email adresi zaten farklı bir yöntemle kayıtlı. Lütfen o yöntemle giriş yapın.",
|
||||
},
|
||||
EmailSignin: {
|
||||
title: "Email Giriş Hatası",
|
||||
description: "Email doğrulama linki gönderilemedi.",
|
||||
},
|
||||
CredentialsSignin: {
|
||||
title: "Giriş Başarısız",
|
||||
description: "Email veya şifreniz hatalı. Lütfen tekrar deneyin.",
|
||||
},
|
||||
SessionRequired: {
|
||||
title: "Oturum Gerekli",
|
||||
description: "Bu sayfaya erişmek için giriş yapmanız gerekiyor.",
|
||||
},
|
||||
Default: {
|
||||
title: "Bir Hata Oluştu",
|
||||
description: "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
},
|
||||
};
|
||||
|
||||
const errorInfo = error ? errorMessages[error] || errorMessages.Default : errorMessages.Default;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
{errorInfo.title}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">{errorInfo.description}</p>
|
||||
{error && (
|
||||
<p className="mt-2 text-center text-xs text-gray-500">
|
||||
Hata kodu: {error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="block w-full text-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Giriş Sayfasına Dön
|
||||
</Link>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="block w-full text-center py-2 px-4 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Yeni Hesap Oluştur
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="block w-full text-center py-2 px-4 text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Ana Sayfaya Dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthErrorPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<ErrorContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
198
app/auth/login/page.tsx
Normal file
198
app/auth/login/page.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError("Giriş başarısız. Email veya şifrenizi kontrol edin. Hesabınızı aktifleştirdiğinizden emin olun.");
|
||||
} else if (result?.ok) {
|
||||
router.push("/dashboard");
|
||||
router.refresh();
|
||||
}
|
||||
} catch {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocialLogin = async (provider: "google" | "github") => {
|
||||
setError("");
|
||||
try {
|
||||
await signIn(provider, {
|
||||
callbackUrl: "/dashboard",
|
||||
});
|
||||
} catch {
|
||||
setError(`${provider === "google" ? "Google" : "GitHub"} ile giriş başarısız.`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Hesabınıza Giriş Yapın
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Veya{" "}
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
yeni hesap oluşturun
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social Login Buttons */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSocialLogin("google")}
|
||||
className="w-full flex items-center justify-center gap-3 py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Google ile Giriş Yap
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSocialLogin("github")}
|
||||
className="w-full flex items-center justify-center gap-3 py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
GitHub ile Giriş Yap
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 text-gray-500">Veya email ile</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email/Password Form */}
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email adresi
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="ornek@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Şifre
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Şifreniz"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
href="/auth/resend-activation"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Aktivasyon emaili tekrar gönder
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
href="/auth/password-reset"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Şifremi unuttum
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Giriş yapılıyor..." : "Giriş Yap"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
203
app/auth/password-reset/confirm/[uid]/[token]/page.tsx
Normal file
203
app/auth/password-reset/confirm/[uid]/[token]/page.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function PasswordResetConfirmPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState({
|
||||
new_password: "",
|
||||
re_new_password: "",
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
// Clear field error when user starts typing
|
||||
if (fieldErrors[e.target.name]) {
|
||||
const newErrors = { ...fieldErrors };
|
||||
delete newErrors[e.target.name];
|
||||
setFieldErrors(newErrors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setLoading(true);
|
||||
|
||||
const { uid, token } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/reset_password_confirm/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uid,
|
||||
token,
|
||||
new_password: formData.new_password,
|
||||
re_new_password: formData.re_new_password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push("/auth/login");
|
||||
}, 3000);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
if (data.new_password || data.re_new_password) {
|
||||
setFieldErrors(data);
|
||||
} else if (data.detail) {
|
||||
setError(data.detail);
|
||||
} else if (data.token) {
|
||||
setError("Şifre sıfırlama linki geçersiz veya süresi dolmuş.");
|
||||
} else {
|
||||
setError("Şifre sıfırlama başarısız. Lütfen bilgilerinizi kontrol edin.");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Password reset confirm error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Şifre Sıfırlama Başarılı!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Şifreniz başarıyla değiştirildi. Artık yeni şifrenizle giriş yapabilirsiniz.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-500">
|
||||
Giriş sayfasına yönlendiriliyorsunuz...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Yeni Şifre Belirle
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Hesabınız için yeni bir şifre oluşturun.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm space-y-4">
|
||||
<div>
|
||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yeni Şifre
|
||||
</label>
|
||||
<input
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.new_password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Minimum 8 karakter"
|
||||
value={formData.new_password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.new_password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.new_password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="re_new_password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yeni Şifre Tekrar
|
||||
</label>
|
||||
<input
|
||||
id="re_new_password"
|
||||
name="re_new_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.re_new_password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Şifrenizi tekrar girin"
|
||||
value={formData.re_new_password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.re_new_password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.re_new_password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Şifre değiştiriliyor..." : "Şifreyi Değiştir"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
153
app/auth/password-reset/page.tsx
Normal file
153
app/auth/password-reset/page.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/reset_password/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.detail || data.email?.[0] || "Email gönderilemedi.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Password reset error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Email Gönderildi!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Şifre sıfırlama linki email adresinize gönderildi. Lütfen email adresinizi kontrol edin.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-xs text-gray-500">
|
||||
Email gelmedi mi? Spam klasörünü kontrol etmeyi unutmayın.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Giriş Sayfasına Dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Şifremi Unuttum
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Email adresinizi girin, size şifre sıfırlama linki gönderelim.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email adresi
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="ornek@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Gönderiliyor..." : "Şifre Sıfırlama Linki Gönder"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<div>
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href="/auth/register"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Hesabınız yok mu? Kayıt olun
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
257
app/auth/register/page.tsx
Normal file
257
app/auth/register/page.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
re_password: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
});
|
||||
const [error, setError] = useState<string>("");
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
// Clear field error when user starts typing
|
||||
if (fieldErrors[e.target.name]) {
|
||||
const newErrors = { ...fieldErrors };
|
||||
delete newErrors[e.target.name];
|
||||
setFieldErrors(newErrors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (data.email || data.password || data.first_name || data.last_name) {
|
||||
setFieldErrors(data);
|
||||
} else if (data.detail) {
|
||||
setError(data.detail);
|
||||
} else {
|
||||
setError("Kayıt başarısız. Lütfen bilgilerinizi kontrol edin.");
|
||||
}
|
||||
} else {
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push("/auth/login");
|
||||
}, 3000);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Registration error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Kayıt Başarılı!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Email adresinize gönderilen aktivasyon linkine tıklayarak hesabınızı aktifleştirin.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-500">
|
||||
Giriş sayfasına yönlendiriliyorsunuz...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Hesap Oluştur
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Veya{" "}
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
mevcut hesabınızla giriş yapın
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ad
|
||||
</label>
|
||||
<input
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
type="text"
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.first_name ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Ad"
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.first_name && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.first_name[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="last_name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Soyad
|
||||
</label>
|
||||
<input
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
type="text"
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.last_name ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Soyad"
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.last_name && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.last_name[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email adresi
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.email ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="ornek@email.com"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.email && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.email[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Şifre
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Minimum 8 karakter"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="re_password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Şifre Tekrar
|
||||
</label>
|
||||
<input
|
||||
id="re_password"
|
||||
name="re_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.re_password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Şifrenizi tekrar girin"
|
||||
value={formData.re_password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.re_password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.re_password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Kayıt yapılıyor..." : "Kayıt Ol"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
140
app/auth/resend-activation/page.tsx
Normal file
140
app/auth/resend-activation/page.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function ResendActivationPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/resend_activation/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.detail || data.email?.[0] || "Email gönderilemedi.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Resend activation error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Email Gönderildi!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Aktivasyon emaili başarıyla gönderildi. Lütfen email adresinizi kontrol edin.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Giriş Sayfasına Dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Aktivasyon Emaili Tekrar Gönder
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Email adresinizi girin, size yeni bir aktivasyon linki gönderelim.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email adresi
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="ornek@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Gönderiliyor..." : "Aktivasyon Emaili Gönder"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
210
app/dashboard/page.tsx
Normal file
210
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
// JWT token'dan expiry bilgisini çıkar
|
||||
function getTokenExpiry(token: string | undefined): string {
|
||||
if (!token) return 'N/A';
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
if (payload.exp) {
|
||||
const expiryDate = new Date(payload.exp * 1000);
|
||||
const now = Date.now();
|
||||
const remainingMs = expiryDate.getTime() - now;
|
||||
const remainingMinutes = Math.floor(remainingMs / 60000);
|
||||
|
||||
if (remainingMs < 0) {
|
||||
return '⚠️ Token süresi dolmuş';
|
||||
} else if (remainingMinutes > 60) {
|
||||
const hours = Math.floor(remainingMinutes / 60);
|
||||
const mins = remainingMinutes % 60;
|
||||
return `${hours} saat ${mins} dakika`;
|
||||
} else {
|
||||
return `${remainingMinutes} dakika`;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return 'N/A';
|
||||
}
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}, [status, router]);
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-gray-600">Yükleniyor...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white rounded-lg shadow p-8">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/profile")}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Profil
|
||||
</button>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/auth/login" })}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
||||
>
|
||||
Çıkış Yap
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{session.error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div className="flex items-center">
|
||||
<svg className="h-5 w-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-medium text-red-800">Session Hatası</p>
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
{session.error === 'RefreshAccessTokenError'
|
||||
? 'Token yenileme başarısız. Lütfen tekrar giriş yapın.'
|
||||
: session.error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/auth/login" })}
|
||||
className="mt-3 text-sm text-red-600 hover:text-red-500 underline"
|
||||
>
|
||||
Tekrar giriş yap →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
||||
Session Bilgileri
|
||||
</h2>
|
||||
<div className="bg-gray-50 rounded-md p-4 space-y-3">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Email:</span>
|
||||
<p className="text-gray-900 mt-1">{session.user?.email || 'N/A'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">İsim:</span>
|
||||
<p className="text-gray-900 mt-1">{session.user?.name || 'Belirtilmemiş'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Access Token:
|
||||
</span>
|
||||
<div className="mt-1 space-y-2">
|
||||
<div className="p-3 bg-white rounded border border-gray-200 break-all">
|
||||
<code className="text-xs text-gray-800">
|
||||
{session.accessToken || '❌ Token bulunamadı'}
|
||||
</code>
|
||||
</div>
|
||||
{session.accessToken && (
|
||||
<p className="text-xs text-gray-600">
|
||||
⏱️ Kalan süre: {getTokenExpiry(session.accessToken)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Refresh Token:
|
||||
</span>
|
||||
<div className="mt-1 space-y-2">
|
||||
<div className="p-3 bg-white rounded border border-gray-200 break-all">
|
||||
<code className="text-xs text-gray-800">
|
||||
{session.refreshToken || '❌ Token bulunamadı'}
|
||||
</code>
|
||||
</div>
|
||||
{session.refreshToken && (
|
||||
<p className="text-xs text-gray-600">
|
||||
⏱️ Kalan süre: {getTokenExpiry(session.refreshToken)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Session Bitiş Tarihi:
|
||||
</span>
|
||||
<p className="text-gray-900 mt-1">
|
||||
{session.expires ? new Date(session.expires).toLocaleString('tr-TR', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium'
|
||||
}) : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">
|
||||
Kalan Süre:
|
||||
</span>
|
||||
<p className="text-gray-900 mt-1">
|
||||
{session.expires ? (() => {
|
||||
const expiryTime = new Date(session.expires).getTime();
|
||||
const now = Date.now();
|
||||
const remainingMs = expiryTime - now;
|
||||
const remainingMinutes = Math.floor(remainingMs / 60000);
|
||||
const remainingHours = Math.floor(remainingMinutes / 60);
|
||||
const remainingDays = Math.floor(remainingHours / 24);
|
||||
|
||||
if (remainingMs < 0) {
|
||||
return '⚠️ Session süresi dolmuş (yeniden giriş yapın)';
|
||||
} else if (remainingDays > 0) {
|
||||
return `${remainingDays} gün ${remainingHours % 24} saat`;
|
||||
} else if (remainingHours > 0) {
|
||||
return `${remainingHours} saat ${remainingMinutes % 60} dakika`;
|
||||
} else {
|
||||
return `${remainingMinutes} dakika`;
|
||||
}
|
||||
})() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
||||
Tüm Session Verisi
|
||||
</h2>
|
||||
<div className="bg-gray-900 rounded-md p-4 overflow-auto">
|
||||
<pre className="text-xs text-green-400">
|
||||
{JSON.stringify(session, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
94
app/globals.css
Normal file
94
app/globals.css
Normal file
@@ -0,0 +1,94 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Preloader */
|
||||
.preloader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Search Overlay & Popup */
|
||||
.search-bg-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9998;
|
||||
display: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.search-bg-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-form-popup {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
z-index: 9999;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
display: none;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.search-form-popup.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-form-popup .close-btn {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.search-form-popup .close-btn:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Blog Section Overflow Fix - Minimal */
|
||||
.blog-card-two .post-img {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blog-card-two .post-body {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.blog-card-two .post-title {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
45
app/layout.tsx
Normal file
45
app/layout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import "./menu-fix.css"; // Fix for menu visibility
|
||||
import AuthProvider from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "IT Solutions | Softora - IT Solutions & Technology",
|
||||
description: "Softora - IT Solutions & Technology HTML Template",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
{/* External Stylesheets */}
|
||||
<link rel="stylesheet" href="/assets/css/animate.css" />
|
||||
<link rel="stylesheet" href="/assets/css/tabler-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/swiper-bundle.min.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
|
||||
{/* External Scripts */}
|
||||
<script src="/assets/js/bootstrap.bundle.min.js" async />
|
||||
<script src="/assets/js/slideToggle.min.js" async />
|
||||
<script src="/assets/js/swiper-bundle.min.js" async />
|
||||
<script src="/assets/js/jarallax.min.js" async />
|
||||
<script src="/assets/js/index.js" async />
|
||||
|
||||
<script src="/assets/js/imagesloaded.pkgd.min.js" async />
|
||||
<script src="/assets/js/isotope.pkgd.min.js" async />
|
||||
<script src="/assets/js/wow.min.js" async />
|
||||
<script src="/assets/js/active.js" async />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
81
app/menu-fix.css
Normal file
81
app/menu-fix.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* Fix for Menu Visibility */
|
||||
.navbar-collapse.show {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Fix for Dropdown Menu Visibility - High Specificity */
|
||||
|
||||
/* When open via click (class .menu-open) */
|
||||
.header-area .navbar-nav li .softora-dd-menu.menu-open {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
||||
/* Mobile specific resets */
|
||||
position: static !important;
|
||||
float: none !important;
|
||||
box-shadow: none !important;
|
||||
padding-left: 20px !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Desktop overrides for .menu-open */
|
||||
@media (min-width: 992px) {
|
||||
.header-area .navbar-nav li .softora-dd-menu.menu-open {
|
||||
position: absolute !important;
|
||||
background: #FFFFFF !important;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1) !important;
|
||||
padding-left: 0 !important;
|
||||
top: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Force hide on mobile when not open */
|
||||
@media (max-width: 991px) {
|
||||
.header-area .navbar-nav li .softora-dd-menu:not(.menu-open) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure items are visible when menu is open */
|
||||
.header-area .navbar-nav li .softora-dd-menu.menu-open li {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Ensure links are visible and have correct color */
|
||||
.header-area .navbar-nav li .softora-dd-menu.menu-open li a {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
color: #222222 !important; /* Force dark color */
|
||||
}
|
||||
|
||||
.header-area .navbar-nav li .softora-dd-menu.menu-open li a:hover {
|
||||
color: #601FEB !important;
|
||||
}
|
||||
|
||||
/* Ensure Toggle Icon is cursor pointer */
|
||||
.dropdown-toggler {
|
||||
cursor: pointer;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Text Color Fixes for top level */
|
||||
.header-area .navbar-brand,
|
||||
.header-area .navbar-nav .nav-link,
|
||||
.header-area .navbar-nav li > a {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.header-area.mobile-menu-open {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.collapse:not(.show) {
|
||||
display: none !important;
|
||||
}
|
||||
117
app/page.tsx
Normal file
117
app/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "./api/auth/[...nextauth]/route";
|
||||
import PreloaderAndSearch from "@/components/PreloaderAndSearch";
|
||||
import Header from "@/components/Header";
|
||||
import HeroSection from "@/components/HeroSection";
|
||||
import CTASection from "@/components/CTASection";
|
||||
import AboutSection from "@/components/AboutSection";
|
||||
import CTABottom from "@/components/CTABottom";
|
||||
import BlogSection from "@/components/BlogSection";
|
||||
import Link from "next/link";
|
||||
import CookieAlert from "@/components/CookieAlert";
|
||||
|
||||
export default async function HomePage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreloaderAndSearch />
|
||||
|
||||
<Header />
|
||||
|
||||
{/* Hero Section - Client Component for animations */}
|
||||
<HeroSection isAuthenticated={!!session} />
|
||||
|
||||
{/* Blog Section - Client Component for WOW.js animations */}
|
||||
<BlogSection />
|
||||
|
||||
|
||||
{/* 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 */}
|
||||
{/* Cookie Alert */}
|
||||
<CookieAlert />
|
||||
|
||||
|
||||
{/* Scroll To Top */}
|
||||
<button id="scrollTopButton" className="softora-scrolltop scrolltop-hide">
|
||||
<i className="ti ti-chevron-up"></i>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
203
app/password/reset/confirm/[uid]/[token]/page.tsx
Normal file
203
app/password/reset/confirm/[uid]/[token]/page.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
export default function PasswordResetConfirmPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState({
|
||||
new_password: "",
|
||||
re_new_password: "",
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
// Clear field error when user starts typing
|
||||
if (fieldErrors[e.target.name]) {
|
||||
const newErrors = { ...fieldErrors };
|
||||
delete newErrors[e.target.name];
|
||||
setFieldErrors(newErrors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setFieldErrors({});
|
||||
setLoading(true);
|
||||
|
||||
const { uid, token } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/reset_password_confirm/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uid,
|
||||
token,
|
||||
new_password: formData.new_password,
|
||||
re_new_password: formData.re_new_password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 204) {
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
router.push("/auth/login");
|
||||
}, 3000);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
if (data.new_password || data.re_new_password) {
|
||||
setFieldErrors(data);
|
||||
} else if (data.detail) {
|
||||
setError(data.detail);
|
||||
} else if (data.token) {
|
||||
setError("Şifre sıfırlama linki geçersiz veya süresi dolmuş.");
|
||||
} else {
|
||||
setError("Şifre sıfırlama başarısız. Lütfen bilgilerinizi kontrol edin.");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Password reset confirm error:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<svg
|
||||
className="h-6 w-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Şifre Sıfırlama Başarılı!
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Şifreniz başarıyla değiştirildi. Artık yeni şifrenizle giriş yapabilirsiniz.
|
||||
</p>
|
||||
<p className="mt-2 text-center text-sm text-gray-500">
|
||||
Giriş sayfasına yönlendiriliyorsunuz...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
Yeni Şifre Belirle
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Hesabınız için yeni bir şifre oluşturun.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm space-y-4">
|
||||
<div>
|
||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yeni Şifre
|
||||
</label>
|
||||
<input
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.new_password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Minimum 8 karakter"
|
||||
value={formData.new_password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.new_password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.new_password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="re_new_password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yeni Şifre Tekrar
|
||||
</label>
|
||||
<input
|
||||
id="re_new_password"
|
||||
name="re_new_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className={`appearance-none relative block w-full px-3 py-2 border ${
|
||||
fieldErrors.re_new_password ? "border-red-300" : "border-gray-300"
|
||||
} placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm`}
|
||||
placeholder="Şifrenizi tekrar girin"
|
||||
value={formData.re_new_password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fieldErrors.re_new_password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.re_new_password[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Şifre değiştiriliyor..." : "Şifreyi Değiştir"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/auth/login"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
Giriş sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
275
app/profile/page.tsx
Normal file
275
app/profile/page.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useState, useEffect, FormEvent } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000/api/v1";
|
||||
|
||||
interface UserData {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
is_active: boolean;
|
||||
date_joined: string;
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}, [status, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
if (!session?.accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/me/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUserData(data);
|
||||
setFormData({
|
||||
first_name: data.first_name || "",
|
||||
last_name: data.last_name || "",
|
||||
});
|
||||
} else if (response.status === 401) {
|
||||
// Token expired, logout
|
||||
signOut({ callbackUrl: "/auth/login" });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching user data:", err);
|
||||
setError("Kullanıcı bilgileri yüklenemedi.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (session?.accessToken) {
|
||||
fetchUserData();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSuccess(false);
|
||||
setUpdating(true);
|
||||
|
||||
if (!session?.accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/users/me/`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUserData(data);
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
} else if (response.status === 401) {
|
||||
signOut({ callbackUrl: "/auth/login" });
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.detail || "Profil güncellenemedi.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Bir hata oluştu. Lütfen tekrar deneyin.");
|
||||
console.error("Update profile error:", err);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
signOut({ callbackUrl: "/auth/login" });
|
||||
};
|
||||
|
||||
if (status === "loading" || loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
<p className="mt-4 text-gray-600">Yükleniyor...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Profil</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Hesap bilgilerinizi görüntüleyin ve güncelleyin.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Info */}
|
||||
{userData && (
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<dl className="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-gray-500">Email</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">{userData.email}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Üyelik Tarihi</dt>
|
||||
<dd className="mt-1 text-sm text-gray-900">
|
||||
{new Date(userData.date_joined).toLocaleDateString("tr-TR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-gray-500">Hesap Durumu</dt>
|
||||
<dd className="mt-1">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
userData.is_active
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{userData.is_active ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Update Form */}
|
||||
<div className="px-4 py-5 sm:p-6 border-t border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">
|
||||
Profil Bilgilerini Güncelle
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="first_name"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Ad
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
id="first_name"
|
||||
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
value={formData.first_name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="last_name"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Soyad
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
id="last_name"
|
||||
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
value={formData.last_name}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="rounded-md bg-green-50 p-4">
|
||||
<p className="text-sm text-green-800">
|
||||
Profil bilgileriniz başarıyla güncellendi!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updating}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{updating ? "Güncelleniyor..." : "Profili Güncelle"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Logout Button */}
|
||||
<div className="px-4 py-5 sm:p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
Çıkış Yap
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
7
app/providers.tsx
Normal file
7
app/providers.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
}
|
||||
Reference in New Issue
Block a user