feat: add hall logo support, fix fonts and build errors

This commit is contained in:
2025-12-07 15:57:01 +03:00
parent a0ed003a13
commit e263d2e235
20 changed files with 567 additions and 411 deletions

View File

@@ -67,7 +67,7 @@ export function EditCustomerForm({ customer }: EditCustomerFormProps) {
toast.success("Müşteri başarıyla güncellendi")
router.push('/dashboard/customers')
router.refresh()
} catch (error) {
} catch {
toast.error("Bir hata oluştu")
} finally {
setLoading(false)
@@ -83,7 +83,7 @@ export function EditCustomerForm({ customer }: EditCustomerFormProps) {
toast.success("Müşteri silindi")
router.push('/dashboard/customers')
router.refresh()
} catch (error) {
} catch {
toast.error("Silme işlemi başarısız")
setLoading(false)
}

View File

@@ -11,7 +11,7 @@ import { Button } from "@/components/ui/button"
import { Plus, Search, Phone, Mail, FileText, Edit } from "lucide-react"
import Link from "next/link"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
export default async function CustomersPage({
searchParams,

View File

@@ -17,7 +17,7 @@ import { createClient } from "@/lib/supabase/client"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
export function CategoryManager({ initialCategories }: { initialCategories: any[] }) {
export function CategoryManager({ initialCategories }: { initialCategories: { id: string; name: string }[] }) {
const [categories, setCategories] = useState(initialCategories)
const [newCategory, setNewCategory] = useState("")
const [loading, setLoading] = useState(false)

View File

@@ -17,7 +17,7 @@ import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { format } from "date-fns"
export function ExpenseForm({ categories }: { categories: any[] }) {
export function ExpenseForm({ categories }: { categories: { id: string; name: string }[] }) {
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()

View File

@@ -65,7 +65,7 @@ export default async function ExpensesPage() {
</TableCell>
</TableRow>
) : (
expenses.map((expense: any) => (
expenses.map((expense) => (
<TableRow key={expense.id}>
<TableCell>
{format(new Date(expense.date), 'd MMMM yyyy', { locale: tr })}

View File

@@ -18,7 +18,7 @@ export async function deleteHall(id: string) {
revalidatePath('/dashboard/halls')
}
export async function updateHall(id: string, data: { name: string; capacity: number; description?: string }) {
export async function updateHall(id: string, data: { name: string; capacity: number; description?: string; logo_url?: string }) {
const supabase = await createClient()
const { error } = await supabase
@@ -27,6 +27,7 @@ export async function updateHall(id: string, data: { name: string; capacity: num
name: data.name,
capacity: data.capacity,
description: data.description,
logo_url: data.logo_url,
})
.eq('id', id)

View File

@@ -0,0 +1,25 @@
import { createClient } from "@/lib/supabase/server"
import { HallForm } from "../hall-form"
import { notFound } from "next/navigation"
export default async function HallEditPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const supabase = await createClient()
const { data: hall } = await supabase.from('halls').select('*').eq('id', id).single()
if (!hall) {
notFound()
}
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Salon Düzenle</h3>
<p className="text-sm text-muted-foreground">
Salon bilgilerini güncelleyin.
</p>
</div>
<HallForm initialData={hall} />
</div>
)
}

View File

@@ -4,10 +4,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import { ImageUpload } from "@/components/ui/image-upload"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -15,7 +15,8 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { createHall } from "./actions"
import { createHall } from "./new/actions"
import { updateHall } from "./[id]/actions"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
@@ -27,29 +28,46 @@ const formSchema = z.object({
capacity: z.coerce.number().min(1, {
message: "Kapasite en az 1 olmalıdır.",
}),
logo_url: z.string().optional(),
description: z.string().optional(),
})
export function HallForm() {
interface HallFormProps {
initialData?: {
id: string
name: string
capacity: number
description?: string | null
logo_url?: string | null
}
}
export function HallForm({ initialData }: HallFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
capacity: 0,
description: "",
name: initialData?.name || "",
capacity: initialData?.capacity || 0,
logo_url: initialData?.logo_url || "",
description: initialData?.description || "",
},
})
async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true)
try {
await createHall(values)
if (initialData) {
await updateHall(initialData.id, values)
toast.success("Salon başarıyla güncellendi")
} else {
await createHall(values)
toast.success("Salon başarıyla oluşturuldu")
}
router.push('/dashboard/halls')
router.refresh()
toast.success("Salon başarıyla oluşturuldu")
} catch (error) {
console.error(error)
toast.error("Bir hata oluştu")
@@ -81,7 +99,24 @@ export function HallForm() {
<FormItem>
<FormLabel>Kapasite</FormLabel>
<FormControl>
<Input type="number" placeholder="500" {...field} />
<Input type="number" placeholder="500" {...field} value={field.value as number} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo_url"
render={({ field }) => (
<FormItem>
<FormLabel>Kurum Logosu</FormLabel>
<FormControl>
<ImageUpload
value={field.value || ""}
onChange={field.onChange}
bucketName="hall-logos"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -101,7 +136,7 @@ export function HallForm() {
)}
/>
<Button type="submit" disabled={loading}>
{loading ? "Kaydediliyor..." : "Kaydet"}
{loading ? "Kaydediliyor..." : (initialData ? "Güncelle" : "Kaydet")}
</Button>
</form>
</Form>

View File

@@ -5,13 +5,14 @@ import { revalidatePath } from "next/cache"
import { logAction } from "@/lib/logger"
export async function createHall(data: { name: string; capacity: number; description?: string }) {
export async function createHall(data: { name: string; capacity: number; description?: string; logo_url?: string }) {
const supabase = await createClient()
const { data: newHall, error } = await supabase.from('halls').insert({
name: data.name,
capacity: data.capacity,
description: data.description,
logo_url: data.logo_url,
}).select().single()
if (error) {

View File

@@ -1,17 +1,16 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { HallForm } from "./hall-form"
import { HallForm } from "../hall-form"
export default function NewHallPage() {
return (
<div className="max-w-2xl mx-auto">
<Card>
<CardHeader>
<CardTitle>Yeni Salon Ekle</CardTitle>
</CardHeader>
<CardContent>
<HallForm />
</CardContent>
</Card>
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Yeni Salon Ekle</h3>
<p className="text-sm text-muted-foreground">
Sisteme yeni bir düğün salonu ekleyin.
</p>
</div>
<HallForm />
</div>
)
}

View File

@@ -34,12 +34,29 @@ export default async function HallsPage() {
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{halls?.map((hall) => (
<Card key={hall.id} className="flex flex-col overflow-hidden transition-all hover:shadow-md">
<CardHeader className="bg-muted/20 pb-4">
<div className="flex items-start justify-between">
<CardTitle className="text-xl font-semibold">{hall.name}</CardTitle>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-primary">
<Edit className="h-4 w-4" />
</Button>
import Image from "next/image"
// ... inside the CardHeader ...
<CardHeader className="bg-muted/20 pb-4 relative">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
{hall.logo_url && (
<div className="relative h-12 w-12 rounded-lg overflow-hidden border bg-background shrink-0">
<Image
src={hall.logo_url}
alt={hall.name}
fill
className="object-cover"
/>
</div>
)}
<CardTitle className="text-xl font-semibold">{hall.name}</CardTitle>
</div>
<Link href={`/dashboard/halls/${hall.id}`}>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-primary">
<Edit className="h-4 w-4" />
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="flex-1 pt-6 space-y-4">

View File

@@ -1,10 +1,9 @@
import { createClient } from "@/lib/supabase/server"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { CalendarDays, CreditCard, Users, DollarSign, TrendingUp, ArrowUpRight, Calendar as CalendarIcon } from "lucide-react"
import { CalendarDays, DollarSign, TrendingUp, Calendar as CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { tr } from "date-fns/locale"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
export default async function DashboardPage() {
const supabase = await createClient()

View File

@@ -41,6 +41,11 @@ export default async function ReservationsPage() {
}
}
const getSingle = (item: any) => {
if (Array.isArray(item)) return item[0]
return item
}
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
@@ -81,28 +86,34 @@ export default async function ReservationsPage() {
</TableCell>
</TableRow>
) : (
reservations?.map((res) => (
<TableRow key={res.id}>
<TableCell className="font-medium">
{format(new Date(res.start_time), 'd MMMM yyyy', { locale: tr })}
</TableCell>
<TableCell>
{format(new Date(res.start_time), 'HH:mm')} - {format(new Date(res.end_time), 'HH:mm')}
</TableCell>
<TableCell>{res.halls?.name}</TableCell>
<TableCell>{res.customers?.full_name}</TableCell>
<TableCell>
{res.packages?.name}
{res.packages?.price && <span className="text-xs text-muted-foreground block">{res.packages.price}</span>}
</TableCell>
<TableCell>{getStatusBadge(res.status || 'pending')}</TableCell>
<TableCell className="text-right">
<Link href={`/dashboard/reservations/${res.id}`}>
<Button variant="ghost" size="sm">Detay</Button>
</Link>
</TableCell>
</TableRow>
))
reservations?.map((res) => {
const hall = getSingle(res.halls)
const customer = getSingle(res.customers)
const pkg = getSingle(res.packages)
return (
<TableRow key={res.id}>
<TableCell className="font-medium">
{format(new Date(res.start_time), 'd MMMM yyyy', { locale: tr })}
</TableCell>
<TableCell>
{format(new Date(res.start_time), 'HH:mm')} - {format(new Date(res.end_time), 'HH:mm')}
</TableCell>
<TableCell>{hall?.name}</TableCell>
<TableCell>{customer?.full_name}</TableCell>
<TableCell>
{pkg?.name}
{pkg?.price && <span className="text-xs text-muted-foreground block">{pkg.price}</span>}
</TableCell>
<TableCell>{getStatusBadge(res.status || 'pending')}</TableCell>
<TableCell className="text-right">
<Link href={`/dashboard/reservations/${res.id}`}>
<Button variant="ghost" size="sm">Detay</Button>
</Link>
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
@@ -115,42 +126,48 @@ export default async function ReservationsPage() {
Henüz rezervasyon yok.
</div>
) : (
reservations?.map((res) => (
<Card key={res.id} className="overflow-hidden">
<CardHeader className="bg-muted/20 p-4 flex flex-row items-center justify-between space-y-0">
<span className="font-semibold">
{format(new Date(res.start_time), 'd MMMM yyyy', { locale: tr })}
</span>
{getStatusBadge(res.status || 'pending')}
</CardHeader>
<CardContent className="p-4 space-y-3">
<div className="flex items-center text-sm">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
{format(new Date(res.start_time), 'HH:mm')} - {format(new Date(res.end_time), 'HH:mm')}
</div>
<div className="flex items-center text-sm">
<MapPin className="mr-2 h-4 w-4 text-muted-foreground" />
{res.halls?.name}
</div>
<div className="flex items-center text-sm">
<User className="mr-2 h-4 w-4 text-muted-foreground" />
{res.customers?.full_name}
</div>
{res.packages?.name && (
reservations?.map((res) => {
const hall = getSingle(res.halls)
const customer = getSingle(res.customers)
const pkg = getSingle(res.packages)
return (
<Card key={res.id} className="overflow-hidden">
<CardHeader className="bg-muted/20 p-4 flex flex-row items-center justify-between space-y-0">
<span className="font-semibold">
{format(new Date(res.start_time), 'd MMMM yyyy', { locale: tr })}
</span>
{getStatusBadge(res.status || 'pending')}
</CardHeader>
<CardContent className="p-4 space-y-3">
<div className="flex items-center text-sm">
<Package className="mr-2 h-4 w-4 text-muted-foreground" />
<span>{res.packages.name}</span>
{res.packages.price && <span className="ml-1 text-muted-foreground">({res.packages.price})</span>}
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />
{format(new Date(res.start_time), 'HH:mm')} - {format(new Date(res.end_time), 'HH:mm')}
</div>
)}
</CardContent>
<CardFooter className="p-4 pt-0">
<Link href={`/dashboard/reservations/${res.id}`} className="w-full">
<Button variant="outline" className="w-full">Detayları Gör</Button>
</Link>
</CardFooter>
</Card>
))
<div className="flex items-center text-sm">
<MapPin className="mr-2 h-4 w-4 text-muted-foreground" />
{hall?.name}
</div>
<div className="flex items-center text-sm">
<User className="mr-2 h-4 w-4 text-muted-foreground" />
{customer?.full_name}
</div>
{pkg?.name && (
<div className="flex items-center text-sm">
<Package className="mr-2 h-4 w-4 text-muted-foreground" />
<span>{pkg.name}</span>
{pkg.price && <span className="ml-1 text-muted-foreground">({pkg.price})</span>}
</div>
)}
</CardContent>
<CardFooter className="p-4 pt-0">
<Link href={`/dashboard/reservations/${res.id}`} className="w-full">
<Button variant="outline" className="w-full">Detayları Gör</Button>
</Link>
</CardFooter>
</Card>
)
})
)}
</div>
</div>

View File

@@ -5,7 +5,7 @@
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-geist-sans);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--color-background: hsl(var(--background));

View File

@@ -1,16 +1,11 @@
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner"
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
export const metadata: Metadata = {
@@ -35,7 +30,7 @@ export default function RootLayout({
<html lang="en">
<body
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${inter.variable} antialiased`}
>
{children}
<Toaster />

View File

@@ -0,0 +1,108 @@
"use client"
import { ChangeEvent, useState } from "react"
import { Button } from "@/components/ui/button"
import { createClient } from "@/lib/supabase/client"
import { ImagePlus, Loader2, X } from "lucide-react"
import Image from "next/image"
import { toast } from "sonner"
interface ImageUploadProps {
value: string
onChange: (url: string) => void
disabled?: boolean
bucketName?: string
}
export function ImageUpload({
value,
onChange,
disabled,
bucketName = "images"
}: ImageUploadProps) {
const [isUploading, setIsUploading] = useState(false)
const supabase = createClient()
const onUpload = async (e: ChangeEvent<HTMLInputElement>) => {
try {
const file = e.target.files?.[0]
if (!file) return
setIsUploading(true)
// 1. Dosya ismini benzersiz yap
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
// 2. Upload
const { error: uploadError } = await supabase.storage
.from(bucketName)
.upload(filePath, file)
if (uploadError) {
throw uploadError
}
// 3. Public URL getir
const { data } = supabase.storage
.from(bucketName)
.getPublicUrl(filePath)
onChange(data.publicUrl)
toast.success("Resim yüklendi")
} catch (error) {
toast.error("Resim yüklenirken hata oluştu")
console.error(error)
} finally {
setIsUploading(false)
}
}
if (value) {
return (
<div className="relative w-[200px] h-[200px] rounded-md overflow-hidden border">
<div className="z-10 absolute top-2 right-2">
<Button
type="button"
onClick={() => onChange("")}
variant="destructive"
size="icon"
disabled={disabled}
>
<X className="h-4 w-4" />
</Button>
</div>
<Image
fill
className="object-cover"
alt="Image"
src={value}
/>
</div>
)
}
return (
<div className="w-[200px] h-[200px] rounded-md border-dashed border-2 flex items-center justify-center bg-muted/50 relative hover:opacity-75 transition disabled:opacity-50 disabled:cursor-not-allowed">
<input
type="file"
accept="image/*"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
onChange={onUpload}
disabled={disabled || isUploading}
/>
{isUploading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<div className="text-xs text-muted-foreground">Yükleniyor...</div>
</div>
) : (
<div className="flex flex-col items-center gap-2">
<ImagePlus className="h-10 w-10 text-muted-foreground" />
<div className="text-xs text-muted-foreground">Resim Yükle</div>
</div>
)}
</div>
)
}

View File

@@ -1,10 +1,6 @@
'use client'
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
// Avatar imports removed
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
@@ -29,11 +25,8 @@ export function UserNav() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
<AvatarFallback>AD</AvatarFallback>
</Avatar>
<Button variant="outline" className="relative">
Seçenekler
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>