364 lines
9.7 KiB
TypeScript
364 lines
9.7 KiB
TypeScript
"use server";
|
||
|
||
import { createClient } from "@/lib/supabase/server";
|
||
import { randomUUID } from "crypto";
|
||
import { revalidatePath } from "next/cache";
|
||
|
||
const PROJECT_TYPES = ["client_project", "side_project"] as const;
|
||
const PROJECT_STATUSES = ["planning", "active", "paused", "completed", "cancelled"] as const;
|
||
const PLANNING_SECTION_CATEGORIES = [
|
||
"overview",
|
||
"problem",
|
||
"goal",
|
||
"audience",
|
||
"scope",
|
||
"design_system",
|
||
"color_palette",
|
||
"typography",
|
||
"assets",
|
||
"notes",
|
||
] as const;
|
||
const PROJECT_ASSETS_BUCKET = "project-assets";
|
||
|
||
function cleanText(value: FormDataEntryValue | null) {
|
||
const text = typeof value === "string" ? value.trim() : "";
|
||
return text.length > 0 ? text : null;
|
||
}
|
||
|
||
function readProjectType(value: FormDataEntryValue | null) {
|
||
const type = typeof value === "string" ? value : "client_project";
|
||
return PROJECT_TYPES.includes(type as (typeof PROJECT_TYPES)[number])
|
||
? type
|
||
: "client_project";
|
||
}
|
||
|
||
function readProjectStatus(value: FormDataEntryValue | null) {
|
||
const status = typeof value === "string" ? value : "planning";
|
||
return PROJECT_STATUSES.includes(status as (typeof PROJECT_STATUSES)[number])
|
||
? status
|
||
: "planning";
|
||
}
|
||
|
||
function readPlanningSectionCategory(value: FormDataEntryValue | null) {
|
||
const category = typeof value === "string" ? value : "overview";
|
||
return PLANNING_SECTION_CATEGORIES.includes(
|
||
category as (typeof PLANNING_SECTION_CATEGORIES)[number],
|
||
)
|
||
? category
|
||
: "overview";
|
||
}
|
||
|
||
function readNumber(value: FormDataEntryValue | null) {
|
||
const number = Number(typeof value === "string" ? value.replace(",", ".") : value);
|
||
return Number.isFinite(number) ? number : null;
|
||
}
|
||
|
||
function readProgress(value: FormDataEntryValue | null) {
|
||
const progress = Math.round(readNumber(value) ?? 0);
|
||
return Math.min(Math.max(progress, 0), 100);
|
||
}
|
||
|
||
async function getCurrentUserId() {
|
||
const supabase = await createClient();
|
||
const {
|
||
data: { user },
|
||
error,
|
||
} = await supabase.auth.getUser();
|
||
|
||
if (error || !user) {
|
||
throw new Error("Proje işlemi için giriş yapmış kullanıcı bulunamadı.");
|
||
}
|
||
|
||
return { supabase, userId: user.id };
|
||
}
|
||
|
||
function readPayload(formData: FormData) {
|
||
const type = readProjectType(formData.get("type"));
|
||
const clientId = cleanText(formData.get("client_id"));
|
||
|
||
return {
|
||
name: cleanText(formData.get("name")),
|
||
type,
|
||
client_id: type === "client_project" ? clientId : null,
|
||
description: cleanText(formData.get("description")),
|
||
status: readProjectStatus(formData.get("status")),
|
||
start_date: cleanText(formData.get("start_date")),
|
||
due_date: cleanText(formData.get("due_date")),
|
||
budget_amount: readNumber(formData.get("budget_amount")),
|
||
currency: cleanText(formData.get("currency")) || "USD",
|
||
progress: readProgress(formData.get("progress")),
|
||
cover_image_alt: cleanText(formData.get("cover_image_alt")),
|
||
};
|
||
}
|
||
|
||
function readImageFile(formData: FormData) {
|
||
const file = formData.get("cover_image");
|
||
|
||
if (!(file instanceof File) || file.size === 0) {
|
||
return null;
|
||
}
|
||
|
||
if (!file.type.startsWith("image/")) {
|
||
throw new Error("Kapak görseli bir görsel dosyası olmalıdır.");
|
||
}
|
||
|
||
return file;
|
||
}
|
||
|
||
function sanitizeFileName(name: string) {
|
||
return name
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9._-]+/g, "-")
|
||
.replace(/^-+|-+$/g, "")
|
||
.slice(0, 120);
|
||
}
|
||
|
||
async function uploadCoverImage({
|
||
supabase,
|
||
userId,
|
||
projectId,
|
||
formData,
|
||
}: {
|
||
supabase: Awaited<ReturnType<typeof createClient>>;
|
||
userId: string;
|
||
projectId: string;
|
||
formData: FormData;
|
||
}) {
|
||
const file = readImageFile(formData);
|
||
|
||
if (!file) {
|
||
return null;
|
||
}
|
||
|
||
const fileName = `${Date.now()}-${sanitizeFileName(file.name) || "cover-image"}`;
|
||
const path = `${userId}/projects/${projectId}/${fileName}`;
|
||
const { error } = await supabase.storage
|
||
.from(PROJECT_ASSETS_BUCKET)
|
||
.upload(path, file, {
|
||
cacheControl: "3600",
|
||
contentType: file.type,
|
||
upsert: true,
|
||
});
|
||
|
||
if (error) {
|
||
throw new Error(`Kapak görseli yüklenemedi: ${error.message}`);
|
||
}
|
||
|
||
return path;
|
||
}
|
||
|
||
export async function createProjectRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const projectId = randomUUID();
|
||
const payload = readPayload(formData);
|
||
|
||
if (!payload.name) {
|
||
throw new Error("Proje adı zorunludur.");
|
||
}
|
||
|
||
const coverImagePath = await uploadCoverImage({
|
||
supabase,
|
||
userId,
|
||
projectId,
|
||
formData,
|
||
});
|
||
|
||
const { error } = await supabase.from("projects").insert({
|
||
id: projectId,
|
||
user_id: userId,
|
||
...payload,
|
||
cover_image_path: coverImagePath,
|
||
});
|
||
|
||
if (error) {
|
||
throw new Error(`Proje eklenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
}
|
||
|
||
export async function updateProjectRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const id = cleanText(formData.get("id"));
|
||
const payload = readPayload(formData);
|
||
|
||
if (!id || !payload.name) {
|
||
throw new Error("Proje güncellemek için proje adı ve kayıt kimliği zorunludur.");
|
||
}
|
||
|
||
const coverImagePath = await uploadCoverImage({
|
||
supabase,
|
||
userId,
|
||
projectId: id,
|
||
formData,
|
||
});
|
||
|
||
const updatePayload = {
|
||
...payload,
|
||
...(coverImagePath ? { cover_image_path: coverImagePath } : {}),
|
||
};
|
||
|
||
const { error } = await supabase
|
||
.from("projects")
|
||
.update(updatePayload)
|
||
.eq("id", id)
|
||
.eq("user_id", userId);
|
||
|
||
if (error) {
|
||
throw new Error(`Proje güncellenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
}
|
||
|
||
export async function completeProjectRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const id = cleanText(formData.get("id"));
|
||
|
||
if (!id) {
|
||
throw new Error("Tamamlanacak proje bulunamadı.");
|
||
}
|
||
|
||
const { error } = await supabase
|
||
.from("projects")
|
||
.update({ status: "completed", progress: 100 })
|
||
.eq("id", id)
|
||
.eq("user_id", userId);
|
||
|
||
if (error) {
|
||
throw new Error(`Proje tamamlanamadı: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
revalidatePath(`/projects/${id}`);
|
||
}
|
||
|
||
function readPlanningSectionPayload(formData: FormData) {
|
||
return {
|
||
project_id: cleanText(formData.get("project_id")),
|
||
category: readPlanningSectionCategory(formData.get("category")),
|
||
title: cleanText(formData.get("title")),
|
||
content: cleanText(formData.get("content")),
|
||
sort_order: Math.round(readNumber(formData.get("sort_order")) ?? 0),
|
||
};
|
||
}
|
||
|
||
export async function createProjectPlanningSectionRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const payload = readPlanningSectionPayload(formData);
|
||
|
||
if (!payload.project_id || !payload.title) {
|
||
throw new Error("Planlama alanı eklemek için proje ve başlık zorunludur.");
|
||
}
|
||
|
||
const { error } = await supabase.from("project_planning_sections").insert({
|
||
user_id: userId,
|
||
project_id: payload.project_id,
|
||
category: payload.category,
|
||
title: payload.title,
|
||
content: payload.content,
|
||
sort_order: payload.sort_order,
|
||
});
|
||
|
||
if (error) {
|
||
throw new Error(`Planlama alanı eklenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
revalidatePath(`/projects/${payload.project_id}`);
|
||
}
|
||
|
||
export async function updateProjectPlanningSectionRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const id = cleanText(formData.get("id"));
|
||
const payload = readPlanningSectionPayload(formData);
|
||
|
||
if (!id || !payload.project_id || !payload.title) {
|
||
throw new Error("Planlama alanını güncellemek için kayıt kimliği, proje ve başlık zorunludur.");
|
||
}
|
||
|
||
const { error } = await supabase
|
||
.from("project_planning_sections")
|
||
.update({
|
||
category: payload.category,
|
||
title: payload.title,
|
||
content: payload.content,
|
||
sort_order: payload.sort_order,
|
||
})
|
||
.eq("id", id)
|
||
.eq("project_id", payload.project_id)
|
||
.eq("user_id", userId);
|
||
|
||
if (error) {
|
||
throw new Error(`Planlama alanı güncellenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
revalidatePath(`/projects/${payload.project_id}`);
|
||
}
|
||
|
||
export async function deleteProjectPlanningSectionRecord(formData: FormData) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
const id = cleanText(formData.get("id"));
|
||
const projectId = cleanText(formData.get("project_id"));
|
||
|
||
if (!id || !projectId) {
|
||
throw new Error("Silinecek planlama alanı bulunamadı.");
|
||
}
|
||
|
||
const { error } = await supabase
|
||
.from("project_planning_sections")
|
||
.delete()
|
||
.eq("id", id)
|
||
.eq("project_id", projectId)
|
||
.eq("user_id", userId);
|
||
|
||
if (error) {
|
||
throw new Error(`Planlama alanı silinemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
revalidatePath(`/projects/${projectId}`);
|
||
}
|
||
|
||
export async function updateRevisionStatus(id: string, projectId: string, status: string) {
|
||
const { supabase } = await getCurrentUserId();
|
||
|
||
const { error } = await supabase
|
||
.from("project_revisions")
|
||
.update({ status })
|
||
.eq("id", id)
|
||
.eq("project_id", projectId);
|
||
|
||
if (error) {
|
||
throw new Error(`Revizyon durumu güncellenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath(`/projects/${projectId}`);
|
||
}
|
||
|
||
export async function updateProjectSettings(projectId: string, progressType: "manual" | "auto", progress: number, revisionQuota: number) {
|
||
const { supabase, userId } = await getCurrentUserId();
|
||
|
||
if (!projectId) {
|
||
throw new Error("Proje ID zorunludur.");
|
||
}
|
||
|
||
const { error } = await supabase
|
||
.from("projects")
|
||
.update({
|
||
progress_type: progressType,
|
||
progress: progress,
|
||
revision_quota: revisionQuota
|
||
})
|
||
.eq("id", projectId)
|
||
.eq("user_id", userId);
|
||
|
||
if (error) {
|
||
throw new Error(`Ayarlar güncellenemedi: ${error.message}`);
|
||
}
|
||
|
||
revalidatePath("/projects");
|
||
revalidatePath(`/projects/${projectId}`);
|
||
}
|