first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:46:42 +03:00
commit 2a5b661443
202 changed files with 49770 additions and 0 deletions

105
frontend/lib/api-proxy.ts Normal file
View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from "next/server"
import { optimizeImage } from "./image-optimizer"
import { getToken } from "next-auth/jwt"
const API_URL = (process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080") + "/api/v1"
export async function handleImageProxyRequest(req: NextRequest, targetEndpoint: string) {
try {
const formData = await req.formData()
const newFormData = new FormData()
// Process fields
// We need to iterate twice: first to collect all config fields, then to process files
const configMap = new Map<string, string>()
for (const [key, value] of formData.entries()) {
if (typeof value === "string") {
configMap.set(key, value)
}
}
for (const [key, value] of formData.entries()) {
if (value instanceof File && value.size > 0) {
// Determine configuration for this file
let width = 0
let height = 0
let quality = 80
let format = "avif"
// Heuristic 1: "image" field maps to root "width", "height", etc.
if (key === "image") {
width = Number(configMap.get("width")) || 0
height = Number(configMap.get("height")) || 0
quality = Number(configMap.get("quality")) || 80
format = configMap.get("format") || "avif"
}
// Heuristic 2: "[prefix]_logo" maps to "[prefix]_width", etc.
else if (key.endsWith("_logo")) {
const prefix = key.replace("_logo", "")
width = Number(configMap.get(`${prefix}_width`)) || 0
height = Number(configMap.get(`${prefix}_height`)) || 0
quality = Number(configMap.get(`${prefix}_quality`)) || 80
format = configMap.get(`${prefix}_format`) || "avif"
}
// If any config is found (or defaults for "image"), optimize
if (key === "image" || key.endsWith("_logo")) {
const buffer = Buffer.from(await value.arrayBuffer())
const { buffer: processedBuffer, contentType, filename } = await optimizeImage(buffer, {
width,
height,
quality,
format,
})
// Create Blob from Buffer
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const blob = new Blob([processedBuffer as any], { type: contentType })
newFormData.append(key, blob, filename)
} else {
// Just forward other files as is
newFormData.append(key, value)
}
} else {
newFormData.append(key, value)
}
}
// Get Auth Token (Server-side)
// NextAuth's getSession might not work in API routes depending on setup,
// getToken is more reliable for middleware/API routes.
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
const headers: HeadersInit = {}
if (token && token.accessToken) {
headers["Authorization"] = `Bearer ${token.accessToken}`
}
// Determine method (POST or PUT)
const method = req.method
const targetUrl = `${API_URL}${targetEndpoint}`
const response = await fetch(targetUrl, {
method: method,
headers: headers,
body: newFormData,
})
const data = await response.json()
if (!response.ok) {
return NextResponse.json(data, { status: response.status })
}
return NextResponse.json(data)
} catch (error: unknown) {
console.error("Proxy Error:", error)
const errorMessage = error instanceof Error ? error.message : "Internal Server Error"
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,21 @@
import { z } from "zod";
export const loginSchema = z.object({
email: z.string().email({ message: "Geçerli bir e-posta adresi giriniz." }),
password: z.string().min(6, { message: "Şifre en az 6 karakter olmalıdır." }),
// Turnstile token is optional in schema but required for submission logic if enabled
turnstileToken: z.string().optional(),
});
export const registerSchema = z.object({
username: z.string().min(3, { message: "Kullanıcı adı en az 3 karakter olmalıdır." }),
email: z.string().email({ message: "Geçerli bir e-posta adresi giriniz." }),
password: z.string().min(8, { message: "Şifre en az 8 karakter olmalıdır." }),
confirmPassword: z.string().min(8, { message: "Şifre tekrarı en az 8 karakter olmalıdır." }),
}).refine((data) => data.password === data.confirmPassword, {
message: "Şifreler eşleşmiyor.",
path: ["confirmPassword"],
});
export type LoginInput = z.infer<typeof loginSchema>;
export type RegisterInput = z.infer<typeof registerSchema>;

View File

@@ -0,0 +1,54 @@
import sharp from "sharp"
export interface ImageOptions {
width?: number
height?: number
quality?: number
format?: string // "webp", "jpeg", "png", "avif"
}
export async function optimizeImage(buffer: Buffer, options: ImageOptions): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
let pipeline = sharp(buffer)
// Resize if width or height is provided and greater than 0
if ((options.width && options.width > 0) || (options.height && options.height > 0)) {
pipeline = pipeline.resize({
width: options.width && options.width > 0 ? options.width : undefined,
height: options.height && options.height > 0 ? options.height : undefined,
fit: "cover", // Or 'contain', 'fill' based on requirement. Cover is usually good for heroes.
withoutEnlargement: true,
})
}
// Default format is AVIF if not specified
const format = options.format?.toLowerCase() || "avif"
const quality = options.quality && options.quality > 0 ? options.quality : 80
switch (format) {
case "jpeg":
case "jpg":
pipeline = pipeline.jpeg({ quality })
break
case "png":
pipeline = pipeline.png({ quality, compressionLevel: 9 }) // PNG quality is different, usually compression level
break
case "webp":
pipeline = pipeline.webp({ quality })
break
case "avif":
pipeline = pipeline.avif({ quality })
break
default:
pipeline = pipeline.avif({ quality })
break
}
const processedBuffer = await pipeline.toBuffer()
const extension = format === "jpg" ? "jpeg" : format
return {
buffer: processedBuffer,
contentType: `image/${extension}`,
filename: `image.${extension}`, // Generic filename, caller can prepend/append if needed
}
}

6
frontend/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}