web sitesi yönetimi
This commit is contained in:
137
components/contact/contact-form.tsx
Normal file
137
components/contact/contact-form.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Loader2, CheckCircle } from "lucide-react"
|
||||
import { contactFormSchema, ContactFormValues } from "@/lib/schemas"
|
||||
import { submitContactForm } from "@/lib/actions/contact"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export function ContactForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
|
||||
const form = useForm<ContactFormValues>({
|
||||
resolver: zodResolver(contactFormSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
surname: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
subject: "",
|
||||
message: "",
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(data: ContactFormValues) {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const response = await submitContactForm(data)
|
||||
if (response.success) {
|
||||
setIsSuccess(true)
|
||||
form.reset()
|
||||
toast.success("Mesajınız başarıyla gönderildi.")
|
||||
} else {
|
||||
toast.error("Hata: " + response.error)
|
||||
}
|
||||
} catch {
|
||||
toast.error("Bir hata oluştu.")
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6 sm:p-8">
|
||||
{isSuccess ? (
|
||||
<div className="flex flex-col items-center justify-center h-full min-h-[400px] text-center space-y-4">
|
||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
||||
<h3 className="text-2xl font-bold">Mesajınız Alındı!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
En kısa sürede size dönüş yapacağız.
|
||||
</p>
|
||||
<Button onClick={() => setIsSuccess(false)} variant="outline">
|
||||
Yeni Mesaj Gönder
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-medium">Adınız</label>
|
||||
<Input id="name" {...form.register("name")} placeholder="Adınız" />
|
||||
{form.formState.errors.name && <p className="text-xs text-red-500">{form.formState.errors.name.message}</p>}
|
||||
</div>
|
||||
<div className="space-y-2 w-[210px]">
|
||||
<label htmlFor="surname" className="text-sm font-medium">Soyadınız</label>
|
||||
<Input id="surname" {...form.register("surname")} placeholder="Soyadınız" />
|
||||
{form.formState.errors.surname && <p className="text-xs text-red-500">{form.formState.errors.surname.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium">E-posta</label>
|
||||
<Input id="email" type="email" {...form.register("email")} placeholder="ornek@sirket.com" />
|
||||
{form.formState.errors.email && <p className="text-xs text-red-500">{form.formState.errors.email.message}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="phone" className="text-sm font-medium">Telefon</label>
|
||||
<div className="relative w-[210px]">
|
||||
<div className="absolute left-3 top-2 text-muted-foreground text-sm flex items-center gap-2 font-medium z-10 select-none pointer-events-none">
|
||||
<span>🇹🇷</span>
|
||||
<span>+90</span>
|
||||
<div className="w-px h-4 bg-border" />
|
||||
</div>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
className="pl-20 font-mono"
|
||||
placeholder="(5XX) XXX XX XX"
|
||||
maxLength={15}
|
||||
{...form.register("phone", {
|
||||
onChange: (e) => {
|
||||
let value = e.target.value.replace(/\D/g, ''); // Remove non-digits
|
||||
if (value.startsWith('90')) value = value.slice(2); // Remove leading 90 if user types it
|
||||
|
||||
// Format: (5XX) XXX XX XX
|
||||
let formattedValue = '';
|
||||
if (value.length > 0) formattedValue += '(' + value.substring(0, 3);
|
||||
if (value.length > 3) formattedValue += ') ' + value.substring(3, 6);
|
||||
if (value.length > 6) formattedValue += ' ' + value.substring(6, 8);
|
||||
if (value.length > 8) formattedValue += ' ' + value.substring(8, 10);
|
||||
|
||||
e.target.value = formattedValue;
|
||||
return e;
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{form.formState.errors.phone && <p className="text-xs text-red-500">{form.formState.errors.phone.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="subject" className="text-sm font-medium">Konu</label>
|
||||
<Input id="subject" {...form.register("subject")} placeholder="Konu" />
|
||||
{form.formState.errors.subject && <p className="text-xs text-red-500">{form.formState.errors.subject.message}</p>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="text-sm font-medium">Mesajınız</label>
|
||||
<Textarea id="message" {...form.register("message")} placeholder="Size nasıl yardımcı olabiliriz?" className="min-h-[120px]" />
|
||||
{form.formState.errors.message && <p className="text-xs text-red-500">{form.formState.errors.message.message}</p>}
|
||||
</div>
|
||||
<Button size="lg" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Mesaj Gönder
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
128
components/dashboard/content-form.tsx
Normal file
128
components/dashboard/content-form.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from "react"
|
||||
import { SiteContent } from "@/types/cms"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { ImageUpload } from "@/components/ui/image-upload" // Ensure this exists or adapt
|
||||
import { updateSiteContent } from "@/app/(dashboard)/dashboard/cms/content/actions"
|
||||
import { toast } from "sonner"
|
||||
import { Save, Loader2 } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface ContentFormProps {
|
||||
initialContent: SiteContent[]
|
||||
}
|
||||
|
||||
const SECTIONS = [
|
||||
{ id: 'general', label: 'Genel Ayarlar' },
|
||||
{ id: 'home', label: 'Anasayfa' },
|
||||
{ id: 'contact', label: 'İletişim' },
|
||||
]
|
||||
|
||||
export function ContentForm({ initialContent }: ContentFormProps) {
|
||||
const [contents, setContents] = useState<SiteContent[]>(initialContent)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [activeSection, setActiveSection] = useState('general')
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setContents(prev => prev.map(item =>
|
||||
item.key === key ? { ...item, value } : item
|
||||
))
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await updateSiteContent(contents)
|
||||
if (result.success) {
|
||||
toast.success("İçerikler başarıyla güncellendi")
|
||||
} else {
|
||||
toast.error(result.error)
|
||||
}
|
||||
} catch {
|
||||
toast.error("Bir hata oluştu")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredContent = contents.filter(item => item.section === activeSection)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Custom Tabs */}
|
||||
<div className="flex space-x-1 rounded-lg bg-muted p-1 overflow-x-auto">
|
||||
{SECTIONS.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
className={cn(
|
||||
"flex-1 justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
activeSection === section.id
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-background/50 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{section.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{SECTIONS.find(s => s.id === activeSection)?.label}</CardTitle>
|
||||
<CardDescription>
|
||||
Bu bölümdeki içerikleri aşağıdan düzenleyebilirsiniz.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{filteredContent.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">Bu bölümde henüz ayar bulunmuyor.</p>
|
||||
)}
|
||||
{filteredContent.map((item) => (
|
||||
<div key={item.key} className="space-y-2">
|
||||
<Label className="capitalize">
|
||||
{item.key.replace(/_/g, ' ').replace(activeSection, '').trim() || item.key}
|
||||
</Label>
|
||||
|
||||
{item.type === 'image_url' ? (
|
||||
<ImageUpload
|
||||
value={item.value}
|
||||
onChange={(url) => handleChange(item.key, url)}
|
||||
onRemove={() => handleChange(item.key, '')}
|
||||
/>
|
||||
) : item.type === 'long_text' || item.type === 'html' || item.key.includes('address') ? (
|
||||
<Textarea
|
||||
value={item.value}
|
||||
onChange={(e) => handleChange(item.key, e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={item.value}
|
||||
onChange={(e) => handleChange(item.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{item.key.includes('map_embed') && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Google Maps'den alınan <iframe> kodunu buraya yapıştırın.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end sticky bottom-6 z-10">
|
||||
<Button onClick={onSubmit} disabled={loading} size="lg" className="shadow-lg">
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
|
||||
Değişiklikleri Kaydet
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LayoutDashboard, Package, ShoppingCart, Users, Settings, Globe, Tags } from "lucide-react"
|
||||
import { LayoutDashboard, Package, ShoppingCart, Users, Settings, Globe, Tags, FileText } from "lucide-react"
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
@@ -31,6 +31,11 @@ const sidebarItems = [
|
||||
href: "/dashboard/users",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "İçerik Yönetimi",
|
||||
href: "/dashboard/cms/content",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "Ayarlar",
|
||||
href: "/dashboard/settings",
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import Link from "next/link"
|
||||
import { getSiteContents } from "@/lib/data"
|
||||
import { Instagram, Youtube, Facebook } from "lucide-react"
|
||||
import { FaTiktok } from "react-icons/fa"
|
||||
|
||||
export async function Footer() {
|
||||
const siteSettings = await getSiteContents()
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="w-full border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 py-12 md:py-24 lg:py-32">
|
||||
<div className="container grid gap-8 px-4 md:px-6 lg:grid-cols-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold tracking-tighter">PARAKASA</span>
|
||||
<span className="text-xl font-bold tracking-tighter">{siteSettings.site_title || "PARAKASA"}</span>
|
||||
</Link>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Yüksek güvenlikli çelik kasa ve para sayma sistemleri için güvenilir çözüm ortağınız.
|
||||
{siteSettings.site_description || "Yüksek güvenlikli çelik kasa ve para sayma sistemleri için güvenilir çözüm ortağınız."}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -44,19 +49,36 @@ export function Footer() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-semibold">Bize Ulaşın</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
İstanbul, Türkiye
|
||||
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
{siteSettings.contact_address || "İstanbul, Türkiye"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
+90 212 000 00 00
|
||||
{siteSettings.contact_phone || "+90 212 000 00 00"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{siteSettings.contact_email}
|
||||
</p>
|
||||
<div className="flex gap-4 mt-4">
|
||||
{/* Social Media Icons would go here */}
|
||||
{siteSettings.social_instagram && (
|
||||
<Link href={siteSettings.social_instagram} target="_blank" className="text-muted-foreground hover:text-foreground">
|
||||
<Instagram className="h-5 w-5" />
|
||||
</Link>
|
||||
)}
|
||||
{siteSettings.social_youtube && (
|
||||
<Link href={siteSettings.social_youtube} target="_blank" className="text-muted-foreground hover:text-foreground">
|
||||
<Youtube className="h-5 w-5" />
|
||||
</Link>
|
||||
)}
|
||||
{siteSettings.social_tiktok && (
|
||||
<Link href={siteSettings.social_tiktok} target="_blank" className="text-muted-foreground hover:text-foreground">
|
||||
<FaTiktok className="h-5 w-5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container px-4 md:px-6 mt-8 pt-8 border-t text-center text-sm text-muted-foreground">
|
||||
© 2026 ParaKasa. Tüm hakları saklıdır.
|
||||
© {new Date().getFullYear()} {siteSettings.site_title || "ParaKasa"}. Tüm hakları saklıdır.
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
||||
@@ -4,19 +4,33 @@ import { Button } from "@/components/ui/button"
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { createClient } from "@/lib/supabase-server"
|
||||
import { getSiteContents } from "@/lib/data"
|
||||
import Image from "next/image"
|
||||
|
||||
export async function Navbar() {
|
||||
const supabase = createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
const siteSettings = await getSiteContents()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center">
|
||||
<div className="mr-8 hidden md:flex">
|
||||
<Link href="/" className="mr-6 flex items-center space-x-2">
|
||||
<span className="text-xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-500 dark:from-slate-100 dark:to-slate-400">
|
||||
PARAKASA
|
||||
</span>
|
||||
{siteSettings.site_logo ? (
|
||||
<div className="relative h-10 w-32">
|
||||
<Image
|
||||
src={siteSettings.site_logo}
|
||||
alt={siteSettings.site_title || "ParaKasa"}
|
||||
fill
|
||||
className="object-contain object-left"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-slate-900 to-slate-500 dark:from-slate-100 dark:to-slate-400">
|
||||
{siteSettings.site_title || "PARAKASA"}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<nav className="flex items-center space-x-6 text-sm font-medium">
|
||||
<Link
|
||||
@@ -52,8 +66,19 @@ export async function Navbar() {
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="pr-0">
|
||||
<Link href="/" className="flex items-center">
|
||||
<span className="font-bold">PARAKASA</span>
|
||||
<Link href="/" className="flex items-center mb-6">
|
||||
{siteSettings.site_logo ? (
|
||||
<div className="relative h-10 w-32">
|
||||
<Image
|
||||
src={siteSettings.site_logo}
|
||||
alt={siteSettings.site_title || "ParaKasa"}
|
||||
fill
|
||||
className="object-contain object-left"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-bold text-xl">{siteSettings.site_title || "PARAKASA"}</span>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex flex-col gap-4 mt-8">
|
||||
<Link href="/products">Ürünler</Link>
|
||||
|
||||
Reference in New Issue
Block a user