Refactor code structure for improved readability and maintainability

This commit is contained in:
Poyraz Avsever
2026-05-08 11:18:55 +03:00
parent e98972597d
commit 9adc34418e
21 changed files with 1194 additions and 393 deletions
+32 -25
View File
@@ -1,12 +1,22 @@
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
type ProfileUpdateData = {
first_name: string
last_name: string
avatar_url?: string
}
export async function updateProfile(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return { error: 'Kullanıcı bulunamadı.' }
}
@@ -15,46 +25,45 @@ export async function updateProfile(formData: FormData) {
const lastName = formData.get('lastName') as string
const avatarFile = formData.get('avatar') as File | null
let avatarUrl = undefined
let avatarUrl: string | undefined
// Upload avatar if a new file is provided
if (avatarFile && avatarFile.size > 0) {
const fileExt = avatarFile.name.split('.').pop()
const fileName = `${user.id}/${Math.random()}.${fileExt}`
const { error: uploadError, data: uploadData } = await supabase.storage
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(fileName, avatarFile, { upsert: true })
if (uploadError) {
return { error: 'Profil fotoğrafı yüklenirken hata oluştu: ' + uploadError.message }
return {
error: `Profil fotoğrafı yüklenirken hata oluştu: ${uploadError.message}`,
}
}
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName)
const {
data: { publicUrl },
} = supabase.storage.from('avatars').getPublicUrl(fileName)
avatarUrl = publicUrl
}
// Update profile
const updateData: any = {
const updateData: ProfileUpdateData = {
first_name: firstName,
last_name: lastName,
}
if (avatarUrl) {
updateData.avatar_url = avatarUrl
}
// Upsert profile in case it doesn't exist yet
const { error } = await supabase
.from('profiles')
.upsert({ id: user.id, ...updateData })
const { error } = await supabase.from('profiles').upsert({
id: user.id,
...updateData,
})
if (error) {
return { error: 'Profil güncellenirken hata oluştu: ' + error.message }
return { error: `Profil güncellenirken hata oluştu: ${error.message}` }
}
revalidatePath('/settings')
@@ -64,17 +73,15 @@ export async function updateProfile(formData: FormData) {
export async function updatePassword(formData: FormData) {
const supabase = await createClient()
const password = formData.get('password') as string
if (!password || password.length < 6) {
return { error: 'Şifre en az 6 karakter olmalıdır.' }
}
const { error } = await supabase.auth.updateUser({
password: password
})
const { error } = await supabase.auth.updateUser({ password })
if (error) {
return { error: 'Şifre güncellenirken hata oluştu: ' + error.message }
return { error: `Şifre güncellenirken hata oluştu: ${error.message}` }
}
return { success: true }
+160 -81
View File
@@ -1,137 +1,201 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Bot, KeyRound, Save, User } from "lucide-react";
import { updatePassword, updateProfile } from "./actions";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Bot, Save, User, KeyRound } from "lucide-react";
import { createClient } from "@/lib/supabase/client";
import { updateProfile, updatePassword } from "./actions";
type AiProvider = "groq" | "ollama" | "openai";
function getInitialAiProvider(): AiProvider {
if (typeof window === "undefined") {
return "groq";
}
const savedProvider = localStorage.getItem("mindspace_ai_provider");
return savedProvider === "ollama" || savedProvider === "openai"
? savedProvider
: "groq";
}
function getInitialApiKey() {
if (typeof window === "undefined") {
return "";
}
return localStorage.getItem("mindspace_api_key") ?? "";
}
export default function SettingsPage() {
const [aiProvider, setAiProvider] = useState("groq");
const [apiKey, setApiKey] = useState("");
const [aiProvider, setAiProvider] = useState<AiProvider>(getInitialAiProvider);
const [apiKey, setApiKey] = useState(getInitialApiKey);
const [saveStatus, setSaveStatus] = useState("");
// Profile state
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [avatarUrl, setAvatarUrl] = useState("");
const [profileSaveStatus, setProfileSaveStatus] = useState("");
const [passwordSaveStatus, setPasswordSaveStatus] = useState("");
const [supabase] = useState(() => createClient());
const formRef = useRef<HTMLFormElement>(null);
const supabase = createClient();
useEffect(() => {
const savedProvider = localStorage.getItem("mindspace_ai_provider");
const savedApiKey = localStorage.getItem("mindspace_api_key");
if (savedProvider) setAiProvider(savedProvider);
if (savedApiKey) setApiKey(savedApiKey);
// Fetch user profile
async function fetchProfile() {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data } = await supabase.from("profiles").select("*").eq("id", user.id).single();
if (data) {
setFirstName(data.first_name || "");
setLastName(data.last_name || "");
setAvatarUrl(data.avatar_url || "");
}
let isActive = true;
const fetchProfile = async () => {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user || !isActive) {
return;
}
}
fetchProfile();
}, []);
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single();
if (!data || !isActive) {
return;
}
setFirstName(data.first_name || "");
setLastName(data.last_name || "");
setAvatarUrl(data.avatar_url || "");
};
void fetchProfile();
return () => {
isActive = false;
};
}, [supabase]);
const handleSaveAI = () => {
localStorage.setItem("mindspace_ai_provider", aiProvider);
localStorage.setItem("mindspace_api_key", apiKey);
setSaveStatus("Ayarlar başarıyla kaydedildi!");
setTimeout(() => setSaveStatus(""), 3000);
window.setTimeout(() => setSaveStatus(""), 3000);
};
const handleProfileAction = async (formData: FormData) => {
const res = await updateProfile(formData);
if (res?.error) {
setProfileSaveStatus("Hata: " + res.error);
const response = await updateProfile(formData);
if (response?.error) {
setProfileSaveStatus(`Hata: ${response.error}`);
} else {
setProfileSaveStatus("Profil başarıyla güncellendi!");
if (formData.get("avatar") && (formData.get("avatar") as File).size > 0) {
const avatar = formData.get("avatar");
if (avatar instanceof File && avatar.size > 0) {
window.location.reload();
}
}
setTimeout(() => setProfileSaveStatus(""), 3000);
window.setTimeout(() => setProfileSaveStatus(""), 3000);
};
const handlePasswordAction = async (formData: FormData) => {
const res = await updatePassword(formData);
if (res?.error) {
setPasswordSaveStatus("Hata: " + res.error);
const response = await updatePassword(formData);
if (response?.error) {
setPasswordSaveStatus(`Hata: ${response.error}`);
} else {
setPasswordSaveStatus("Şifre başarıyla güncellendi!");
formRef.current?.reset();
}
setTimeout(() => setPasswordSaveStatus(""), 3000);
window.setTimeout(() => setPasswordSaveStatus(""), 3000);
};
return (
<div className="flex flex-col gap-6 p-4 max-w-2xl mx-auto w-full">
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 p-4">
<div>
<h1 className="text-3xl font-bold text-foreground">Ayarlar</h1>
<p className="text-muted-foreground mt-1">
<p className="mt-1 text-muted-foreground">
Kullanıcı profili ve yapay zeka asistanı yapılandırmanızı yönetin.
</p>
</div>
{/* Profile Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<User className="w-6 h-6 text-primary" />
<User className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Kullanıcı Profili</h2>
</div>
<form action={handleProfileAction} className="space-y-4">
<div className="flex items-center gap-4 mb-6">
<div className="mb-6 flex items-center gap-4">
{avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="w-16 h-16 rounded-full object-cover border border-border" />
<>
{/* Avatar URL dış kaynaklı olduğu için bu önizlemede native img kullanıyoruz. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={avatarUrl}
alt="Avatar"
className="h-16 w-16 rounded-full border border-border object-cover"
/>
</>
) : (
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center border border-border">
<User className="w-8 h-8 text-muted-foreground" />
</div>
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-muted">
<User className="h-8 w-8 text-muted-foreground" />
</div>
)}
<div className="space-y-1 flex-1">
<div className="flex-1 space-y-1">
<Label htmlFor="avatar">Profil Fotoğrafı Yükle</Label>
<Input id="avatar" name="avatar" type="file" accept="image/*" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">Ad</Label>
<Input id="firstName" name="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
<Input
id="firstName"
name="firstName"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Soyad</Label>
<Input id="lastName" name="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} />
<Input
id="lastName"
name="lastName"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
</div>
</div>
<div className="flex items-center gap-4 mt-4">
<div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" />
<Save className="mr-2 h-4 w-4" />
Profili Kaydet
</Button>
{profileSaveStatus && (
<span className={`text-sm ${profileSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
<span
className={`text-sm ${
profileSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{profileSaveStatus}
</span>
)}
@@ -139,26 +203,38 @@ export default function SettingsPage() {
</form>
</div>
{/* Password Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<KeyRound className="w-6 h-6 text-primary" />
<KeyRound className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Şifre Değiştir</h2>
</div>
<form ref={formRef} action={handlePasswordAction} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="password">Yeni Şifre</Label>
<Input id="password" name="password" type="password" minLength={6} placeholder="En az 6 karakter" required />
<Input
id="password"
name="password"
type="password"
minLength={6}
placeholder="En az 6 karakter"
required
/>
</div>
<div className="flex items-center gap-4 mt-4">
<div className="mt-4 flex items-center gap-4">
<Button type="submit" className="w-max">
<Save className="w-4 h-4 mr-2" />
<Save className="mr-2 h-4 w-4" />
Şifreyi Güncelle
</Button>
{passwordSaveStatus && (
<span className={`text-sm ${passwordSaveStatus.startsWith("Hata") ? "text-red-500" : "text-green-600 dark:text-green-400"}`}>
<span
className={`text-sm ${
passwordSaveStatus.startsWith("Hata")
? "text-red-500"
: "text-green-600 dark:text-green-400"
}`}
>
{passwordSaveStatus}
</span>
)}
@@ -166,31 +242,34 @@ export default function SettingsPage() {
</form>
</div>
{/* AI Settings */}
<div className="bg-card border border-border rounded-xl p-6 shadow-sm space-y-6 flex flex-col">
<div className="flex flex-col space-y-6 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="flex items-center gap-3 border-b border-border/50 pb-4">
<Bot className="w-6 h-6 text-primary" />
<Bot className="h-6 w-6 text-primary" />
<h2 className="text-xl font-semibold">Terapist (AI) Ayarları</h2>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label>AI Sağlayıcısı</Label>
<Select value={aiProvider} onValueChange={setAiProvider}>
<Select
value={aiProvider}
onValueChange={(value) => setAiProvider(value as AiProvider)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sağlayıcı seçin" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ollama">
Ollama (Yerel & Gizlilik Odaklı)
Ollama (Yerel ve Gizlilik Odaklı)
</SelectItem>
<SelectItem value="openai">OpenAI (GPT-4o vb.)</SelectItem>
<SelectItem value="groq">Groq (Llama-3 Bulut)</SelectItem>
<SelectItem value="groq">Groq (Llama 3 Bulut)</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Gizliliğiniz için yerel Ollama önerilir. Sunucunuzda çalışmayan
durumlarda OpenAI veya Groq gibi bulut çözümlerine geçebilirsiniz.
durumlarda OpenAI veya Groq gibi bulut çözümlerine
geçebilirsiniz.
</p>
</div>
@@ -200,19 +279,19 @@ export default function SettingsPage() {
<Input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
onChange={(event) => setApiKey(event.target.value)}
placeholder="sk-..."
/>
<p className="text-xs text-muted-foreground">
Anahtarınız sadece tarayıcınızın kendi local hafızasında güvenle
saklanır, hiçbir sunucuya kaydedilmez.
Anahtarınız sadece tarayıcınızın kendi local hafızasında saklanır;
herhangi bir sunucuya kaydedilmez.
</p>
</div>
)}
<div className="flex items-center gap-4 mt-4">
<Button onClick={handleSaveAI} className="w-max">
<Save className="w-4 h-4 mr-2" />
<div className="mt-4 flex items-center gap-4">
<Button type="button" onClick={handleSaveAI} className="w-max">
<Save className="mr-2 h-4 w-4" />
Kaydet
</Button>
{saveStatus && (