From d320787df638e529c45cdf796697256b34e6f34d Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Fri, 30 Jan 2026 00:09:16 +0300 Subject: [PATCH] =?UTF-8?q?Sms=20Rehber=20eklemesi,Mobil=20uyumluluk=20aya?= =?UTF-8?q?rlar=C4=B1,ileti=C5=9Fim=20sayfas=C4=B1=20d=C3=BCzenlemeler=20v?= =?UTF-8?q?b.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(dashboard)/dashboard/products/actions.ts | 7 +- app/(dashboard)/dashboard/products/page.tsx | 2 +- app/(dashboard)/dashboard/sms/logs/page.tsx | 6 +- app/(dashboard)/loading.tsx | 27 ++ app/(public)/contact/page.tsx | 13 +- app/(public)/loading.tsx | 33 ++ app/(public)/products/[id]/page.tsx | 105 +++++ app/(public)/products/page.tsx | 11 +- components/contact/contact-form.tsx | 8 +- components/dashboard/product-form.tsx | 50 ++- components/dashboard/settings-tabs.tsx | 2 +- components/dashboard/sms-page-client.tsx | 424 +++++++++++++++--- components/dashboard/user-nav.tsx | 14 +- components/product/product-gallery.tsx | 94 ++++ components/ui/skeleton.tsx | 15 + lib/sms/templates.ts | 57 +++ lint-results.txt | Bin 0 -> 1754 bytes migrations/add_product_code.sql | 5 + migrations/add_sms_templates.sql | 14 + notlar.txt | 14 + 20 files changed, 800 insertions(+), 101 deletions(-) create mode 100644 app/(dashboard)/loading.tsx create mode 100644 app/(public)/loading.tsx create mode 100644 app/(public)/products/[id]/page.tsx create mode 100644 components/product/product-gallery.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 lib/sms/templates.ts create mode 100644 lint-results.txt create mode 100644 migrations/add_product_code.sql create mode 100644 migrations/add_sms_templates.sql create mode 100644 notlar.txt diff --git a/app/(dashboard)/dashboard/products/actions.ts b/app/(dashboard)/dashboard/products/actions.ts index d04c349..9df2ac4 100644 --- a/app/(dashboard)/dashboard/products/actions.ts +++ b/app/(dashboard)/dashboard/products/actions.ts @@ -11,6 +11,7 @@ interface ProductData { image_url?: string is_active?: boolean images?: string[] + product_code?: string } export async function createProduct(data: ProductData) { @@ -24,7 +25,8 @@ export async function createProduct(data: ProductData) { description: data.description, price: data.price, image_url: data.image_url, // Main image (can be first of images) - is_active: data.is_active ?? true + is_active: data.is_active ?? true, + product_code: data.product_code }).select().single() if (error) throw error @@ -62,7 +64,8 @@ export async function updateProduct(id: number, data: ProductData) { description: data.description, price: data.price, image_url: data.image_url, - is_active: data.is_active + is_active: data.is_active, + product_code: data.product_code }).eq("id", id) if (error) throw error diff --git a/app/(dashboard)/dashboard/products/page.tsx b/app/(dashboard)/dashboard/products/page.tsx index c786446..3a540fa 100644 --- a/app/(dashboard)/dashboard/products/page.tsx +++ b/app/(dashboard)/dashboard/products/page.tsx @@ -32,7 +32,7 @@ export default async function ProductsPage() { -
+
diff --git a/app/(dashboard)/dashboard/sms/logs/page.tsx b/app/(dashboard)/dashboard/sms/logs/page.tsx index 85608a0..e322e0a 100644 --- a/app/(dashboard)/dashboard/sms/logs/page.tsx +++ b/app/(dashboard)/dashboard/sms/logs/page.tsx @@ -19,11 +19,11 @@ export default async function SmsLogsPage() { } return ( -
-

SMS Geçmişi

+
+

SMS Geçmişi

Son gönderilen mesajların durumu.

-
+
diff --git a/app/(dashboard)/loading.tsx b/app/(dashboard)/loading.tsx new file mode 100644 index 0000000..374ea70 --- /dev/null +++ b/app/(dashboard)/loading.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default function DashboardLoading() { + return ( +
+
+
+ + +
+
+ +
+
+
+ + + + +
+
+ + +
+
+ ) +} diff --git a/app/(public)/contact/page.tsx b/app/(public)/contact/page.tsx index 52907fc..c7ea949 100644 --- a/app/(public)/contact/page.tsx +++ b/app/(public)/contact/page.tsx @@ -9,14 +9,14 @@ export default async function ContactPage() { return (
-
-

İletişime Geçin

+
+

İletişime Geçin

Sorularınız, teklif talepleriniz veya teknik destek için bize ulaşın.

-
+

İletişim Bilgileri

@@ -34,7 +34,12 @@ export default async function ContactPage() {

Telefon

- {siteSettings.contact_phone || "+90 (212) 555 00 00"} + + {siteSettings.contact_phone || "+90 (212) 555 00 00"} +

diff --git a/app/(public)/loading.tsx b/app/(public)/loading.tsx new file mode 100644 index 0000000..1a8941b --- /dev/null +++ b/app/(public)/loading.tsx @@ -0,0 +1,33 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default function PublicLoading() { + return ( +
+
+
+ +
+ + + + +
+
+
+
+ + +
+
+ + + +
+
+ +
+
+
+
+ ) +} diff --git a/app/(public)/products/[id]/page.tsx b/app/(public)/products/[id]/page.tsx new file mode 100644 index 0000000..3d2ac23 --- /dev/null +++ b/app/(public)/products/[id]/page.tsx @@ -0,0 +1,105 @@ +import { createClient } from "@/lib/supabase-server" +import { getSiteContents } from "@/lib/data" +import { notFound } from "next/navigation" +import Image from "next/image" +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Phone } from "lucide-react" +import { ProductGallery } from "@/components/product/product-gallery" + +async function getProduct(id: string) { + const supabase = createClient() + + // Fetch product + const { data: product, error } = await supabase + .from("products") + .select("*") + .eq("id", id) + .eq("is_active", true) + .single() + + if (error || !product) { + return null + } + + // Fetch images + const { data: images } = await supabase + .from("product_images") + .select("*") + .eq("product_id", id) + .order("display_order", { ascending: true }) + + return { ...product, images: images || [] } +} + +export default async function ProductPage({ params }: { params: { id: string } }) { + const product = await getProduct(params.id) + const siteSettings = await getSiteContents() + const whatsappPhone = siteSettings.contact_phone ? siteSettings.contact_phone.replace(/\s+/g, '') : "905555555555" + + if (!product) { + notFound() + } + + // Combine main image and gallery images for a full list, filtering duplicates if necessary + // Logic: If gallery images exist, use them. If not, fallback to product.image_url. + // If product.image_url is in product_images, we might duplicate, but let's just use all distinct. + + let allImages: string[] = [] + if (product.images && product.images.length > 0) { + allImages = product.images.map((img: { image_url: string }) => img.image_url) + } else if (product.image_url) { + allImages = [product.image_url] + } + + return ( +
+
+ {/* Image Gallery Section */} + + + {/* Product Info Section */} +
+
+
+ + {product.category} + + {product.product_code && ( + + Kod: {product.product_code} + + )} +
+

+ {product.name} +

+ {/* NO PRICE DISPLAY as requested */} +
+ +
+

Ürün Açıklaması

+

+ {product.description || "Bu ürün için henüz detaylı açıklama eklenmemiştir."} +

+
+ +
+
+ +
+

+ * Bu ürün hakkında detaylı bilgi ve fiyat teklifi almak için bizimle iletişime geçebilirsiniz. +

+
+
+
+
+ ) +} diff --git a/app/(public)/products/page.tsx b/app/(public)/products/page.tsx index db16c5b..700d45b 100644 --- a/app/(public)/products/page.tsx +++ b/app/(public)/products/page.tsx @@ -2,6 +2,7 @@ import { createClient } from "@/lib/supabase-server" import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" +import Link from "next/link" import Image from "next/image" @@ -57,14 +58,20 @@ export default async function ProductsPage() {
{product.category} - ₺{product.price} + {product.product_code && ( + #{product.product_code} + )}
{product.name}
- + )) diff --git a/components/contact/contact-form.tsx b/components/contact/contact-form.tsx index ffeaaf1..18d0363 100644 --- a/components/contact/contact-form.tsx +++ b/components/contact/contact-form.tsx @@ -62,19 +62,19 @@ export function ContactForm() {
) : (
-
+
{form.formState.errors.name &&

{form.formState.errors.name.message}

}
-
+
{form.formState.errors.surname &&

{form.formState.errors.surname.message}

}
-
+
@@ -82,7 +82,7 @@ export function ContactForm() {
-
+
🇹🇷 +90 diff --git a/components/dashboard/product-form.tsx b/components/dashboard/product-form.tsx index 7c273e6..d89004b 100644 --- a/components/dashboard/product-form.tsx +++ b/components/dashboard/product-form.tsx @@ -31,10 +31,11 @@ import imageCompression from 'browser-image-compression' import { createClient } from "@/lib/supabase-browser" import Image from "next/image" -import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions" +import { createProduct, updateProduct, deleteProductImage } from "@/app/(dashboard)/dashboard/products/actions" const productSchema = z.object({ name: z.string().min(2, "Ürün adı en az 2 karakter olmalıdır"), + product_code: z.string().optional(), category: z.string().min(1, "Kategori seçiniz"), description: z.string().optional(), price: z.coerce.number().min(0, "Fiyat 0'dan küçük olamaz"), @@ -50,6 +51,7 @@ type ProductFormValues = z.infer interface Product { id: number name: string + product_code?: string | null category: string description: string | null price: number @@ -80,6 +82,7 @@ export function ProductForm({ initialData }: ProductFormProps) { resolver: zodResolver(productSchema) as Resolver, defaultValues: initialData ? { name: initialData.name, + product_code: initialData.product_code || "", category: initialData.category, description: initialData.description || "", price: initialData.price, @@ -88,6 +91,7 @@ export function ProductForm({ initialData }: ProductFormProps) { images: initialData.image_url ? [initialData.image_url] : [] } : { name: "", + product_code: "", category: "", description: "", price: 0, @@ -159,16 +163,31 @@ export function ProductForm({ initialData }: ProductFormProps) { } } - const removeImage = (index: number) => { + const removeImage = async (index: number) => { + const imageToDelete = previewImages[index] + + // If editing an existing product and image is from server (starts with http/https usually) + if (initialData && imageToDelete.startsWith("http")) { + const result = await deleteProductImage(imageToDelete, initialData.id) + if (!result.success) { + toast.error("Resim silinirken hata oluştu") + return + } + toast.success("Resim silindi") + } + const currentImages = [...form.getValues("images") || []] - currentImages.splice(index, 1) - form.setValue("images", currentImages) - if (currentImages.length > 0) { - form.setValue("image_url", currentImages[0]) + // Filter out the deleted image URL if it matches + const newImages = currentImages.filter(url => url !== imageToDelete) + + form.setValue("images", newImages) + + if (newImages.length > 0) { + form.setValue("image_url", newImages[0]) } else { form.setValue("image_url", "") } - setPreviewImages(currentImages) + setPreviewImages(newImages) } async function onSubmit(data: ProductFormValues) { @@ -224,7 +243,7 @@ export function ProductForm({ initialData }: ProductFormProps) { />
-
+
)} /> + ( + + Ürün Kodu + + + + + + )} + /> +
+
- + İçerik Yönetimi Kullanıcılar SMS / Bildirimler diff --git a/components/dashboard/sms-page-client.tsx b/components/dashboard/sms-page-client.tsx index b6cc335..48c65a6 100644 --- a/components/dashboard/sms-page-client.tsx +++ b/components/dashboard/sms-page-client.tsx @@ -1,19 +1,36 @@ -'use client' +"use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" import { Customer } from "@/types/customer" import { sendBulkSms } from "@/lib/sms/actions" +import { getTemplates, createTemplate, deleteTemplate, SmsTemplate } from "@/lib/sms/templates" import { Button } from "@/components/ui/button" - import { Textarea } from "@/components/ui/textarea" import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" -import { Loader2, Send } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Loader2, Send, Users, Save, Trash2, BookOpen, Smartphone } from "lucide-react" import { toast } from "sonner" import { ScrollArea } from "@/components/ui/scroll-area" @@ -29,7 +46,17 @@ interface SmsPageProps { export default function SmsPageClient({ customers }: SmsPageProps) { const [loading, setLoading] = useState(false) - const [selectAll, setSelectAll] = useState(false) + const [templates, setTemplates] = useState([]) + + // Template Management States + const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false) + const [newTemplateTitle, setNewTemplateTitle] = useState("") + const [newTemplateMessage, setNewTemplateMessage] = useState("") + const [templateLoading, setTemplateLoading] = useState(false) + + // Contact Picker States + const [isContactModalOpen, setIsContactModalOpen] = useState(false) + const [searchTerm, setSearchTerm] = useState("") const form = useForm>({ resolver: zodResolver(formSchema), @@ -40,19 +67,117 @@ export default function SmsPageClient({ customers }: SmsPageProps) { }, }) - const handleSelectAll = (checked: boolean) => { - setSelectAll(checked) - if (checked) { - const allPhones = customers.map(c => c.phone).filter(Boolean) as string[] - form.setValue('selectedCustomers', allPhones) - } else { - form.setValue('selectedCustomers', []) + async function handleNativeContactPicker() { + if (!('contacts' in navigator && 'ContactsManager' in window)) { + toast.error("Rehber özelliği desteklenmiyor (HTTPS gerekli olabilir).") + return } + + try { + const props = ['tel']; + const opts = { multiple: true }; + const contacts = await (navigator as any).contacts.select(props, opts); + + if (contacts && contacts.length > 0) { + const newNumbers = contacts.map((contact: any) => { + const phone = contact.tel?.[0] + return phone ? phone.replace(/\s/g, '') : null; + }).filter(Boolean).join(", "); + + if (newNumbers) { + const current = form.getValues("manualNumbers"); + const updated = current ? `${current}, ${newNumbers}` : newNumbers; + form.setValue("manualNumbers", updated); + toast.success(`${contacts.length} numara eklendi.`); + } + } + } catch (ex) { + console.error(ex); + } + } + + // Load templates on mount + useEffect(() => { + loadTemplates() + }, []) + + async function loadTemplates() { + const result = await getTemplates() + if (result.success && result.data) { + setTemplates(result.data) + } + } + + async function handleSaveTemplate() { + if (!newTemplateTitle || !newTemplateMessage) { + toast.error("Başlık ve mesaj zorunludur") + return + } + + setTemplateLoading(true) + const result = await createTemplate(newTemplateTitle, newTemplateMessage) + setTemplateLoading(false) + + if (result.success) { + toast.success("Şablon kaydedildi") + setNewTemplateTitle("") + setNewTemplateMessage("") + setIsTemplateModalOpen(false) + loadTemplates() + } else { + toast.error(result.error || "Şablon kaydedilemedi") + } + } + + async function handleDeleteTemplate(id: string) { + if (!confirm("Bu şablonu silmek istediğinize emin misiniz?")) return + + const result = await deleteTemplate(id) + if (result.success) { + toast.success("Şablon silindi") + loadTemplates() + } else { + toast.error("Şablon silinemedi") + } + } + + const handleSelectTemplate = (id: string) => { + const template = templates.find(t => t.id === id) + if (template) { + form.setValue("message", template.message) + } + } + + // Filter customers for contact picker + const filteredCustomers = customers.filter(c => + c.full_name?.toLowerCase().includes(searchTerm.toLowerCase()) || + c.phone?.includes(searchTerm) + ) + + const toggleCustomerSelection = (phone: string) => { + const current = form.getValues("selectedCustomers") || [] + if (current.includes(phone)) { + form.setValue("selectedCustomers", current.filter(p => p !== phone)) + } else { + form.setValue("selectedCustomers", [...current, phone]) + } + } + + const selectAllFiltered = () => { + const current = form.getValues("selectedCustomers") || [] + const newPhones = filteredCustomers.map(c => c.phone).filter(Boolean) as string[] + // Merge unique + const merged = Array.from(new Set([...current, ...newPhones])) + form.setValue("selectedCustomers", merged) + } + + const deselectAll = () => { + form.setValue("selectedCustomers", []) } async function onSubmit(values: z.infer) { const manualPhones = values.manualNumbers - ?.split(/[,\n]/) // Split by comma or newline + ?.split(/[,\n]/) .map(p => p.trim()) .filter(p => p !== "") || [] @@ -69,8 +194,11 @@ export default function SmsPageClient({ customers }: SmsPageProps) { const result = await sendBulkSms(allPhones, values.message) if (result.success) { toast.success(result.message) - form.reset() - setSelectAll(false) + form.reset({ + manualNumbers: "", + message: "", + selectedCustomers: [] + }) } else { toast.error(result.error || "SMS gönderilirken hata oluştu") } @@ -84,11 +212,11 @@ export default function SmsPageClient({ customers }: SmsPageProps) { const watchedSelected = form.watch("selectedCustomers") || [] return ( -
-

SMS Gönderimi

+
+

SMS Gönderimi

- + Mesaj Bilgileri @@ -97,17 +225,85 @@ export default function SmsPageClient({ customers }: SmsPageProps) { -
- -