From 3562e10713562e617d247d77e796e69eb3fb5f57 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Tue, 20 Jan 2026 21:58:41 +0300 Subject: [PATCH] feat: implement merchant dashboard, secure auth, and short_id system - Added dedicated merchant dashboard with analytics and transactions - Implemented API Key based authentication for merchants - Introduced 8-character Short IDs for merchants to use in URLs - Refactored checkout and payment intent APIs to support multi-gateway - Enhanced Landing Page with Merchant Portal access and marketing copy - Fixed Next.js 15 async params build issues - Updated internal branding to P2CGateway - Added AyrisTech credits to footer --- app/admin/analytics/page.tsx | 113 +++-- app/admin/customers/page.tsx | 99 +++-- app/admin/docs/page.tsx | 184 ++++++++ app/admin/layout.tsx | 10 +- app/admin/merchants/new/page.tsx | 222 ++++++++++ app/admin/merchants/page.tsx | 349 +++++++++++++++ app/admin/page.tsx | 208 +++------ app/admin/settings/page.tsx | 4 +- app/admin/transactions/page.tsx | 83 +++- app/api/create-payment-intent/route.ts | 88 +++- app/api/merchants/[id]/route.ts | 91 ++++ app/api/merchants/auth/route.ts | 60 +++ app/api/merchants/logout/route.ts | 23 + app/api/merchants/route.ts | 70 +++ app/api/mock-complete-payment/route.ts | 2 +- app/api/update-transaction-info/route.ts | 2 +- app/api/webhooks/stripe/route.ts | 42 +- app/checkout/page.tsx | 108 +++-- app/layout.tsx | 4 +- app/login/page.tsx | 2 +- .../[id]/(dashboard)/integration/page.tsx | 159 +++++++ app/merchant/[id]/(dashboard)/layout.tsx | 100 +++++ app/merchant/[id]/(dashboard)/page.tsx | 255 +++++++++++ .../[id]/(dashboard)/transactions/page.tsx | 150 +++++++ app/merchant/[id]/login/page.tsx | 103 +++++ app/not-found.tsx | 43 ++ app/page.tsx | 217 +++++++--- components/admin/AddMerchantModal.tsx | 146 +++++++ components/admin/AnalyticsBarChart.tsx | 81 ++++ components/admin/CustomerSearch.tsx | 41 ++ components/admin/QueryRangeSelector.tsx | 29 ++ components/admin/TransactionChart.tsx | 87 ++++ components/admin/TransactionSearch.tsx | 41 ++ components/admin/TransactionStatusFilter.tsx | 40 ++ components/merchant/MerchantSidebar.tsx | 91 ++++ docs/db_schema.sql | 21 +- docs/update_schema_merchants.sql | 17 + docs/update_schema_providers.sql | 11 + docs/update_schema_short_ids.sql | 25 ++ lib/payment-providers.ts | 78 ++++ lib/supabase-admin.ts | 9 + lib/supabase.ts | 5 - package-lock.json | 401 +++++++++++++++++- package.json | 1 + middleware.ts => proxy.ts | 4 +- utils/supabase/{middleware.ts => proxy.ts} | 0 46 files changed, 3505 insertions(+), 414 deletions(-) create mode 100644 app/admin/docs/page.tsx create mode 100644 app/admin/merchants/new/page.tsx create mode 100644 app/admin/merchants/page.tsx create mode 100644 app/api/merchants/[id]/route.ts create mode 100644 app/api/merchants/auth/route.ts create mode 100644 app/api/merchants/logout/route.ts create mode 100644 app/api/merchants/route.ts create mode 100644 app/merchant/[id]/(dashboard)/integration/page.tsx create mode 100644 app/merchant/[id]/(dashboard)/layout.tsx create mode 100644 app/merchant/[id]/(dashboard)/page.tsx create mode 100644 app/merchant/[id]/(dashboard)/transactions/page.tsx create mode 100644 app/merchant/[id]/login/page.tsx create mode 100644 app/not-found.tsx create mode 100644 components/admin/AddMerchantModal.tsx create mode 100644 components/admin/AnalyticsBarChart.tsx create mode 100644 components/admin/CustomerSearch.tsx create mode 100644 components/admin/QueryRangeSelector.tsx create mode 100644 components/admin/TransactionChart.tsx create mode 100644 components/admin/TransactionSearch.tsx create mode 100644 components/admin/TransactionStatusFilter.tsx create mode 100644 components/merchant/MerchantSidebar.tsx create mode 100644 docs/update_schema_merchants.sql create mode 100644 docs/update_schema_providers.sql create mode 100644 docs/update_schema_short_ids.sql create mode 100644 lib/payment-providers.ts create mode 100644 lib/supabase-admin.ts rename middleware.ts => proxy.ts (82%) rename utils/supabase/{middleware.ts => proxy.ts} (100%) diff --git a/app/admin/analytics/page.tsx b/app/admin/analytics/page.tsx index 4321327..126bcad 100644 --- a/app/admin/analytics/page.tsx +++ b/app/admin/analytics/page.tsx @@ -9,11 +9,13 @@ import { Smartphone, Calendar } from 'lucide-react'; -import { supabaseAdmin } from '@/lib/supabase'; +import { supabaseAdmin } from '@/lib/supabase-admin'; import { format, subDays } from 'date-fns'; import { tr } from 'date-fns/locale'; +import AnalyticsBarChart from '@/components/admin/AnalyticsBarChart'; +import QueryRangeSelector from '@/components/admin/QueryRangeSelector'; -async function getAnalyticsData() { +async function getAnalyticsData(rangeDays: number = 12) { const { data: transactions, error } = await supabaseAdmin .from('transactions') .select('*') @@ -25,10 +27,9 @@ async function getAnalyticsData() { const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0); const avgOrderValue = successfulTransactions.length > 0 ? totalRevenue / successfulTransactions.length : 0; - // Monthly data for chart (grouped by month or last 12 periods) - // To keep it simple and meaningful, let's show last 12 days for "Gelir Trendi" - const last12Periods = Array.from({ length: 12 }, (_, i) => { - const d = subDays(new Date(), 11 - i); + // Monthly data for chart (grouped by month or last N periods) + const lastPeriods = Array.from({ length: rangeDays }, (_, i) => { + const d = subDays(new Date(), (rangeDays - 1) - i); return { date: d.toISOString().split('T')[0], label: format(d, 'd MMM', { locale: tr }), @@ -38,7 +39,7 @@ async function getAnalyticsData() { successfulTransactions.forEach(t => { const dateStr = new Date(t.created_at).toISOString().split('T')[0]; - const periodMatch = last12Periods.find(p => p.date === dateStr); + const periodMatch = lastPeriods.find(p => p.date === dateStr); if (periodMatch) { periodMatch.amount += Number(t.amount); } @@ -47,14 +48,18 @@ async function getAnalyticsData() { return { totalRevenue, avgOrderValue, - chartData: last12Periods, + chartData: lastPeriods, totalCount: transactions.length, successCount: successfulTransactions.length, }; } -export default async function AnalyticsPage() { - const data = await getAnalyticsData(); +export default async function AnalyticsPage(props: { + searchParams: Promise<{ range?: string }>; +}) { + const searchParams = await props.searchParams; + const range = searchParams.range ? parseInt(searchParams.range) : 12; + const data = await getAnalyticsData(range); if (!data) return
Veriler hazırlanıyor...
; @@ -76,10 +81,7 @@ export default async function AnalyticsPage() {

Sistem performans verileri

- +
@@ -113,63 +115,44 @@ export default async function AnalyticsPage() { -
- {data.chartData.map((d, i) => { - const h = (d.amount / maxChartAmount) * 90 + 5; // 5% to 95% - return ( -
-
-
- {d.amount.toLocaleString('tr-TR')} ₺ -
-
-
- {d.label} + +
+
+ + {/* Breakdown Grid */} +
+
+

Cihaz Dağılımı

+
+ {[ + { label: 'Mobil', value: '64%', icon: Smartphone, color: 'bg-blue-600', width: '64%' }, + { label: 'Masaüstü', value: '28%', icon: Monitor, color: 'bg-indigo-400', width: '28%' }, + { label: 'Tablet', value: '8%', icon: Globe, color: 'bg-indigo-100', width: '8%' }, + ].map((item, i) => ( +
+
+
+ + {item.label}
+ {item.value}
- ); - })} +
+
+
+
+ ))}
- {/* Breakdown Grid */} -
-
-

Cihaz Dağılımı

-
- {[ - { label: 'Mobil', value: '64%', icon: Smartphone, color: 'bg-blue-600', width: '64%' }, - { label: 'Masaüstü', value: '28%', icon: Monitor, color: 'bg-indigo-400', width: '28%' }, - { label: 'Tablet', value: '8%', icon: Globe, color: 'bg-indigo-100', width: '8%' }, - ].map((item, i) => ( -
-
-
- - {item.label} -
- {item.value} -
-
-
-
-
- ))} -
-
- -
-
-

Analizleriniz hazır!
Bu ay başarılı bir grafik çiziyorsunuz.

- -
- +
+
+

Analizleriniz hazır!
Bu ay başarılı bir grafik çiziyorsunuz.

+
+
diff --git a/app/admin/customers/page.tsx b/app/admin/customers/page.tsx index 66d7e19..737713b 100644 --- a/app/admin/customers/page.tsx +++ b/app/admin/customers/page.tsx @@ -8,12 +8,15 @@ import { MoreHorizontal, ArrowUpRight } from 'lucide-react'; -import { supabaseAdmin } from '@/lib/supabase'; +import { supabaseAdmin } from '@/lib/supabase-admin'; -async function getCustomers() { +import CustomerSearch from '@/components/admin/CustomerSearch'; + +async function getFilteredCustomers(queryText?: string) { const { data: transactions, error } = await supabaseAdmin .from('transactions') - .select('*'); + .select('*') + .order('created_at', { ascending: false }); if (error || !transactions) return null; @@ -21,7 +24,10 @@ async function getCustomers() { const customerMap = new Map(); transactions.forEach(t => { - const key = t.customer_name || t.customer_phone || 'Unknown'; + // We use a combination of name and phone as a key if possible, + // fallback to whichever is available + const key = (t.customer_phone || t.customer_name || 'Unknown').toLowerCase().trim(); + if (!customerMap.has(key)) { customerMap.set(key, { id: t.id, @@ -29,27 +35,45 @@ async function getCustomers() { phone: t.customer_phone || 'Telefon Yok', spent: 0, orders: 0, + lastOrder: t.created_at, status: 'New' }); } + const c = customerMap.get(key); c.orders += 1; if (t.status === 'succeeded') { c.spent += Number(t.amount); } + // Update last order date if this transaction is newer + if (new Date(t.created_at) > new Date(c.lastOrder)) { + c.lastOrder = t.created_at; + } }); - const customers = Array.from(customerMap.values()).map(c => { - if (c.orders > 5) c.status = 'High Value'; + let customers = Array.from(customerMap.values()).map(c => { + if (c.orders > 5 && c.spent > 1000) c.status = 'High Value'; else if (c.orders > 1) c.status = 'Active'; return c; }); + // Client-side search (since customers are derived) + if (queryText) { + const q = queryText.toLowerCase(); + customers = customers.filter(c => + c.name.toLowerCase().includes(q) || + c.phone.toLowerCase().includes(q) + ); + } + return customers; } -export default async function CustomersPage() { - const customers = await getCustomers(); +export default async function CustomersPage(props: { + searchParams: Promise<{ q?: string }>; +}) { + const searchParams = await props.searchParams; + const customers = await getFilteredCustomers(searchParams.q); if (!customers) return
Müşteriler yükleniyor...
; @@ -61,21 +85,21 @@ export default async function CustomersPage() {

Müşteriler

Müşteri portföyünüzü yönetin

- +
+
+ Canlı Veritabanı Bağlantısı +
{/* Stats */}
-
-
+
+

{customers.length.toLocaleString('tr-TR')}

-

Toplam Müşteri

+

Sorgulanan Müşteri

@@ -83,8 +107,8 @@ export default async function CustomersPage() {
-

Gerçek

-

Canlı Veri

+

%{((customers.filter(c => c.status === 'High Value' || c.status === 'Active').length / (customers.length || 1)) * 100).toFixed(0)}

+

Bağlılık Oranı

@@ -93,7 +117,7 @@ export default async function CustomersPage() {

{customers.filter(c => c.phone !== 'Telefon Yok').length}

-

Telefon Kayıtlı

+

İletişim Bilgili

@@ -101,17 +125,10 @@ export default async function CustomersPage() { {/* List */}
-
- - + +
+ Sıralama: En Son Ödeme
-
@@ -122,7 +139,7 @@ export default async function CustomersPage() { Segment Sipariş Toplam Harcama - Aksiyonlar + Durum @@ -130,7 +147,7 @@ export default async function CustomersPage() {
-
+
{customer.name.slice(0, 2).toUpperCase()}
@@ -141,7 +158,7 @@ export default async function CustomersPage() { @@ -151,7 +168,7 @@ export default async function CustomersPage() { - {customer.orders} + {customer.orders} İşlem @@ -160,12 +177,12 @@ export default async function CustomersPage() {
- - +
+ Son İşlem + + {new Date(customer.lastOrder).toLocaleDateString('tr-TR')} + +
@@ -173,6 +190,12 @@ export default async function CustomersPage() {
+ {customers.length === 0 && ( +
+ +

Müşteri bulunamadı

+
+ )}
); diff --git a/app/admin/docs/page.tsx b/app/admin/docs/page.tsx new file mode 100644 index 0000000..fd54eff --- /dev/null +++ b/app/admin/docs/page.tsx @@ -0,0 +1,184 @@ +'use client'; + +import React from 'react'; +import { + Code2, + Terminal, + Globe, + Webhook, + Copy, + Check, + ArrowRight, + Zap, + ShieldCheck, + MessageSquare +} from 'lucide-react'; + +export default function DocumentationPage() { + const [copied, setCopied] = React.useState(null); + + const copyToClipboard = (text: string, id: string) => { + navigator.clipboard.writeText(text); + setCopied(id); + setTimeout(() => setCopied(null), 2000); + }; + + const checkoutUrlCode = `https://p2cgateway.com/checkout?merchant_id=YOUR_MERCHANT_ID&amount=100¤cy=TRY&ref_id=ORDER_123&callback_url=https://yoursite.com/success`; + + return ( +
+ {/* Header */} +
+

API Dokümantasyonu

+

P2CGateway Entegrasyon Rehberi

+
+ + {/* Quick Start Card */} +
+
+
+
+ +
+

Hızlı Başlangıç

+
+

+ P2CGateway'i projenize entegre etmek için sadece bir URL oluşturmanız yeterlidir. + Karmaşık SDK'lar veya kütüphanelerle uğraşmanıza gerek yok. +

+
+
+
+ + {/* Integration Steps */} +
+ {/* Method 1: Checkout Redirect */} +
+
+
+ +
+

1. Ödeme Sayfasına Yönlendirme

+
+ +

+ Müşterinizi ödeme yapması için aşağıdaki URL yapısını kullanarak P2CGateway checkout sayfasına yönlendirin. +

+ +
+
+ + {checkoutUrlCode} + + +
+
+ +
+

Parametreler

+
+ {[ + { key: 'merchant_id', desc: 'Firma ID\'niz (Firmalar sayfasından alabilirsiniz)' }, + { key: 'amount', desc: 'Ödeme tutarı (Örn: 100.00)' }, + { key: 'currency', desc: 'Para birimi (TRY, USD, EUR)' }, + { key: 'ref_id', desc: 'Sizin sisteminizdeki sipariş numarası' }, + { key: 'callback_url', desc: 'Ödeme sonrası yönlendirilecek adres' }, + ].map((p) => ( +
+ {p.key} + {p.desc} +
+ ))} +
+
+
+ + {/* Method 2: Webhooks */} +
+
+
+ +
+

2. Webhook Bildirimleri

+
+ +

+ Ödeme tamamlandığında sistemimiz otomatik olarak firmanıza tanımlı olan Webhook URL'ine bir POST isteği gönderir. +

+ +
+
+ JSON Payload Örneği +
+
+
+
+
+
+
+                            {`{
+  "status": "succeeded",
+  "transaction_id": "tx_821...",
+  "ref_id": "ORDER-123",
+  "amount": 100.00,
+  "currency": "TRY",
+  "customer": {
+    "name": "Ahmet Yılmaz",
+    "phone": "555..."
+  }
+}`}
+                        
+
+ +
+

+ Güvenlik Notu +

+

+ Webhook isteklerinin P2CGateway'den geldiğini doğrulamak için API Key'inizi HTTP başlığında (X-P2C-Signature) kontrol etmelisiniz. +

+
+
+
+ + {/* API Resources Section */} +
+
+
+ +
+

Geliştirici Kaynakları

+
+ +
+ + + +
+
+
+ ); +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 54ab882..88907c0 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -14,7 +14,9 @@ import { Bell, MessageSquare, ChevronDown, - Wallet + Wallet, + Building2, + Code2 } from 'lucide-react'; import Image from 'next/image'; import { createClient } from '@/utils/supabase/client'; // Assuming a client-side Supabase client utility @@ -36,9 +38,11 @@ export default function AdminLayout({ const navItems = [ { label: 'Genel Bakış', icon: LayoutDashboard, href: '/admin' }, + { label: 'Firmalar', icon: Building2, href: '/admin/merchants' }, { label: 'İşlemler', icon: CreditCard, href: '/admin/transactions' }, { label: 'Müşteriler', icon: Users, href: '/admin/customers' }, { label: 'Analizler', icon: BarChart3, href: '/admin/analytics' }, + { label: 'Dokümantasyon', icon: Code2, href: '/admin/docs' }, { label: 'Ayarlar', icon: Settings, href: '/admin/settings' }, ]; @@ -51,8 +55,8 @@ export default function AdminLayout({
-

froyd Admin

-

Yönetim Paneli

+

P2CGateway Admin

+

Merkezi Yönetim Paneli

diff --git a/app/admin/merchants/new/page.tsx b/app/admin/merchants/new/page.tsx new file mode 100644 index 0000000..b5ff132 --- /dev/null +++ b/app/admin/merchants/new/page.tsx @@ -0,0 +1,222 @@ +'use client'; + +import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { + ArrowLeft, + Building2, + Globe, + CheckCircle2, + Loader2, + ShieldCheck, + Smartphone, + CreditCard +} from 'lucide-react'; +import Link from 'next/link'; + +export default function NewMerchantPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [name, setName] = useState(''); + const [webhookUrl, setWebhookUrl] = useState(''); + const [paymentProvider, setPaymentProvider] = useState('stripe'); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + const response = await fetch('/api/merchants', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + webhook_url: webhookUrl, + payment_provider: paymentProvider + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Firma eklenemedi.'); + } + + setSuccess(true); + setTimeout(() => { + router.push('/admin/merchants'); + router.refresh(); + }, 2000); + } catch (err: any) { + alert(err.message || 'Firma eklenirken bir hata oluştu.'); + } finally { + setIsLoading(false); + } + }; + + if (success) { + return ( +
+
+
+ +
+
+

Firma Başarıyla Oluşturuldu!

+

Yönlendiriliyorsunuz...

+
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ + + +
+

Yeni Firma Kaydı

+

Sisteme yeni bir işletme entegre edin

+
+
+
+ +
+ {/* Form Column */} +
+
+ {/* Name Input */} +
+ +
+
+ +
+ setName(e.target.value)} + className="w-full pl-16 pr-8 py-5 bg-gray-50 border-2 border-transparent focus:border-blue-100 focus:bg-white rounded-3xl text-base font-bold text-gray-900 outline-none transition-all placeholder:text-gray-300" + /> +
+
+ + {/* Provider Selection */} +
+ +
+ {[ + { id: 'stripe', name: 'Stripe', icon: CreditCard, desc: 'Global kart ödemeleri' }, + { id: 'cryptomus', name: 'Cryptomus', icon: Globe, desc: 'Kripto Para' }, + { id: 'nuvei', name: 'Nuvei', icon: ShieldCheck, desc: 'E-commerce Experts' }, + { id: 'paykings', name: 'PayKings', icon: ShieldCheck, desc: 'High Risk Specialist' }, + { id: 'seurionpay', name: 'SecurionPay', icon: CreditCard, desc: 'Simple Payments' }, + ].map((p) => ( + + ))} +
+
+ + {/* Webhook Input */} +
+ +
+
+ +
+ setWebhookUrl(e.target.value)} + className="w-full pl-16 pr-8 py-5 bg-gray-50 border-2 border-transparent focus:border-blue-100 focus:bg-white rounded-3xl text-base font-bold text-gray-900 outline-none transition-all placeholder:text-gray-300" + /> +
+

+ Ödeme sonuçlarını gerçek zamanlı almak için firmanın API uç noktasını buraya girebilirsiniz. (Opsiyonel) +

+
+ + {/* Submit Button */} +
+ +
+
+
+ + {/* Info Column */} +
+
+

Hızlı Başlangıç

+
    +
  • +
    + +
    +

    Firma ismi sisteme özel bir ID ile kaydedilir.

    +
  • +
  • +
    + +
    +

    Kayıt sonrası hemen ödeme linki oluşturulur.

    +
  • +
  • +
    + +
    +

    Tüm işlemler 256-bit SSL ile korunur.

    +
  • +
+
+ +
+
+ +

Desteklenen Kartlar

+
+

+ Visa, Mastercard, Troy ve tüm yerel banka kartları ile ödeme alabilirsiniz. +

+
+
+
+
+ ); +} diff --git a/app/admin/merchants/page.tsx b/app/admin/merchants/page.tsx new file mode 100644 index 0000000..e626b04 --- /dev/null +++ b/app/admin/merchants/page.tsx @@ -0,0 +1,349 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { + Plus, + Building2, + Copy, + ExternalLink, + MoreVertical, + Globe, + Check, + Pencil, + Trash2, + X +} from 'lucide-react'; + +export default function MerchantsPage() { + const [merchants, setMerchants] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [copiedId, setCopiedId] = useState(null); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editingMerchant, setEditingMerchant] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + + useEffect(() => { + fetchMerchants(); + }, []); + + const fetchMerchants = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/merchants'); + const data = await response.json(); + if (!response.ok) throw new Error(data.error); + setMerchants(data); + } catch (err) { + console.error('Fetch error:', err); + } finally { + setIsLoading(false); + } + }; + + const copyToClipboard = (text: string, id: string) => { + navigator.clipboard.writeText(text); + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); + }; + + const handleEditClick = (merchant: any) => { + setEditingMerchant({ ...merchant }); + setIsEditModalOpen(true); + }; + + const handleUpdateMerchant = async (e: React.FormEvent) => { + e.preventDefault(); + setIsUpdating(true); + try { + const response = await fetch(`/api/merchants/${editingMerchant.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: editingMerchant.name, + webhook_url: editingMerchant.webhook_url, + payment_provider: editingMerchant.payment_provider, + provider_config: editingMerchant.provider_config + }) + }); + if (!response.ok) throw new Error('Güncelleme başarısız.'); + + await fetchMerchants(); + setIsEditModalOpen(false); + setEditingMerchant(null); + } catch (err: any) { + alert(err.message); + } finally { + setIsUpdating(false); + } + }; + + const handleDeleteMerchant = async (id: string) => { + if (!confirm('Bu firmayı silmek istediğinize emin misiniz? Bu işlem geri alınamaz.')) return; + + try { + const response = await fetch(`/api/merchants/${id}`, { + method: 'DELETE' + }); + if (!response.ok) throw new Error('Silme işlemi başarısız.'); + await fetchMerchants(); + } catch (err: any) { + alert(err.message); + } + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Firmalar (Merchants)

+

Ödeme alan tüm işletmeler

+
+
+ + + + Yeni Firma Ekle + +
+ + {/* Merchant Cards Grid */} +
+ {isLoading ? ( +
+
+
+ ) : merchants.map((m) => { + const identifier = m.short_id || m.id; + const paymentLink = typeof window !== 'undefined' + ? `${window.location.origin}/checkout?merchant_id=${identifier}&amount=0` + : `https://p2cgateway.com/checkout?merchant_id=${identifier}&amount=0`; + + return ( +
+
+
+
+ {m.name.substring(0, 1).toUpperCase()} +
+
+

{m.name}

+
+ ID: {identifier} + {m.short_id && Short Link Active} +
+
+
+
+ + +
+
+ +
+ {/* API Key Section */} +
+ +
+
+ {m.api_key || '••••••••••••••••'} +
+ +
+
+ + {/* Webhook Section */} +
+ +
+ {m.webhook_url || 'Ayarlanmamış'} + +
+
+ + {/* Payment Link Section */} +
+ +
+
+ {paymentLink} +
+ +
+

Bu linki tutar ve referans ekleyerek değiştirebilirsiniz.

+
+
+ +
+
+
+ Aktif +
+ + Tüm İşlemleri Gör + + + + Firma Paneli + + +
+
+ ); + })} + + {!isLoading && merchants.length === 0 && ( +
+
+ +
+
+

Henüz firma bulunmuyor

+

İlk firmanızı ekleyerek ödeme almaya başlayın.

+
+ + + Firma Ekleyerek Başlayın + +
+ )} +
+ + {/* Edit Modal */} + {isEditModalOpen && ( +
+
+
+
+
+

Firmayı Düzenle

+

Firma bilgilerini güncelle

+
+ +
+ +
+
+
+ +
+ + setEditingMerchant({ ...editingMerchant, name: e.target.value })} + placeholder="Örn: Ayris Teknoloji" + className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-bold text-gray-900 placeholder:text-gray-300" + /> +
+
+ +
+ +
+ {[ + { id: 'stripe', name: 'Stripe' }, + { id: 'cryptomus', name: 'Cryptomus' }, + { id: 'nuvei', name: 'Nuvei' }, + { id: 'paykings', name: 'PayKings' }, + { id: 'securionpay', name: 'SecurionPay' }, + ].map((p) => ( + + ))} +
+
+ +
+ +
+ + setEditingMerchant({ ...editingMerchant, webhook_url: e.target.value })} + placeholder="https://siteniz.com/webhook" + className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-bold text-gray-900 placeholder:text-gray-300" + /> +
+

Ödeme başarılı olduğunda bu adrese bildirim gönderilecektir.

+
+
+ +
+ + +
+
+
+
+
+ )} +
+ ); +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 984c0a7..8cd7124 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { supabaseAdmin } from '@/lib/supabase'; +import { supabaseAdmin } from '@/lib/supabase-admin'; import { TrendingUp, TrendingDown, @@ -11,8 +11,10 @@ import { import { format } from 'date-fns'; import { tr } from 'date-fns/locale'; import Link from 'next/link'; +import TransactionChart from '@/components/admin/TransactionChart'; +import QueryRangeSelector from '@/components/admin/QueryRangeSelector'; -async function getStats() { +async function getStats(rangeDays: number = 30) { const { data: transactions, error } = await supabaseAdmin .from('transactions') .select('*') @@ -34,11 +36,11 @@ async function getStats() { .map(t => t.customer_name || t.customer_phone) ).size; - // Last 30 days chart data - const last30Days = Array.from({ length: 30 }, (_, i) => { + // Dynamic chart data based on range + const chartData = Array.from({ length: rangeDays }, (_, i) => { const d = new Date(); d.setHours(0, 0, 0, 0); - d.setDate(d.getDate() - (29 - i)); + d.setDate(d.getDate() - (rangeDays - 1 - i)); return { date: d.toISOString().split('T')[0], displayDate: format(d, 'd MMM', { locale: tr }), @@ -48,7 +50,7 @@ async function getStats() { successfulTransactions.forEach(t => { const dateStr = new Date(t.created_at).toISOString().split('T')[0]; - const dayMatch = last30Days.find(d => d.date === dateStr); + const dayMatch = chartData.find(d => d.date === dateStr); if (dayMatch) { dayMatch.amount += Number(t.amount); } @@ -62,12 +64,16 @@ async function getStats() { successRate, totalCount, uniqueCustomers, - chartData: last30Days + chartData }; } -export default async function AdminDashboard() { - const stats = await getStats(); +export default async function AdminDashboard(props: { + searchParams: Promise<{ range?: string }>; +}) { + const searchParams = await props.searchParams; + const range = searchParams.range ? parseInt(searchParams.range) : 30; + const stats = await getStats(range); if (!stats) { return
Henüz bir işlem verisi bulunamadı.
; @@ -96,163 +102,65 @@ export default async function AdminDashboard() {
- {/* Total Customers */} + {/* Successful Transactions */}
-

Toplam Müşteri

-
- -
-
-
-

{stats.uniqueCustomers.toLocaleString('tr-TR')}

-
- - {stats.totalCount} toplam işlem kaydı -
-
-
- - {/* Pending Payments */} -
-
-

Bekleyen Ödemeler

-
- -
-
-
-

{stats.pendingCount}

-
- - İşlem Bekliyor onay aşamasında -
-
-
- - {/* Success Rate */} -
-
-

Başarı Oranı

+

İşlem Sayısı

-

{stats.successRate.toFixed(1)}%

-
- - Optimized ödeme dönüşüm oranı +

{stats.successfulCount}

+

Tamamlanan Ödeme

+
+
+ + {/* Conversion Rate */} +
+
+

Başarı Oranı

+
+
+
+

%{stats.successRate.toFixed(1)}

+

{stats.totalCount} Toplam İstek

+
+
+ + {/* Unique Customers */} +
+
+

Tekil Müşteri

+
+ +
+
+
+

{stats.uniqueCustomers}

+

Farklı Ödeme Kaynağı

+
{/* Middle Section: Charts */} -
- {/* Transaction Volume Line Chart */} -
-
-
-

İşlem Hacmi

-

Son 30 günlük toplam hacim

-
- +
+
+
+

İşlem Hacmi

+

Son {range} günlük toplam hacim

- -
- {/* Dynamic SVG Chart */} - {(() => { - const maxAmount = Math.max(...stats.chartData.map(d => d.amount), 100); - const points = stats.chartData.map((d, i) => ({ - x: (i / 29) * 100, // 0 to 100% - y: 100 - (d.amount / maxAmount) * 80 - 10 // 10 to 90% (lower y is higher value) - })); - - const dLine = points.reduce((acc, p, i) => - i === 0 ? `M 0 ${p.y}` : `${acc} L ${p.x} ${p.y}`, '' - ); - - const dArea = `${dLine} L 100 100 L 0 100 Z`; - - return ( - - - - - - - - - - - ); - })()} - -
- {stats.chartData[0].displayDate} - {stats.chartData[10].displayDate} - {stats.chartData[20].displayDate} - Bugün -
+
+ +

+ {range} günlük veri gösteriliyor +

- {/* Revenue by Source Donut Chart */} -
-

Kaynak Bazlı Ciro

- -
-
- - - - - -
-

{stats.totalRevenue.toLocaleString('tr-TR', { maximumFractionDigits: 0 })} ₺

-

Toplam Ciro

-
-
- -
-
-
- Kart (60%) -
-
-
- Havale (20%) -
-
-
- Cüzdan (15%) -
-
-
- Diğer (5%) -
-
-
-
+
{/* Bottom Section: Recent Transactions Table */} @@ -280,7 +188,7 @@ export default async function AdminDashboard() {
- #{t.stripe_pi_id.slice(-8).toUpperCase()} + #{t.stripe_pi_id?.slice(-8).toUpperCase() || 'EXTERNAL'} {t.id.slice(0, 8)}
diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx index 6f0a2fa..c059329 100644 --- a/app/admin/settings/page.tsx +++ b/app/admin/settings/page.tsx @@ -39,11 +39,11 @@ export default function SettingsPage() {
- +
- +
diff --git a/app/admin/transactions/page.tsx b/app/admin/transactions/page.tsx index ca443e7..868ef61 100644 --- a/app/admin/transactions/page.tsx +++ b/app/admin/transactions/page.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { supabaseAdmin } from '@/lib/supabase'; +import { supabaseAdmin } from '@/lib/supabase-admin'; import { Search, Filter, @@ -9,37 +9,72 @@ import { } from 'lucide-react'; import { format } from 'date-fns'; import { tr } from 'date-fns/locale'; +import TransactionSearch from '@/components/admin/TransactionSearch'; +import TransactionStatusFilter from '@/components/admin/TransactionStatusFilter'; -async function getTransactions() { - const { data, error } = await supabaseAdmin +async function getTransactions(filters: { merchant_id?: string; q?: string; status?: string }) { + let query = supabaseAdmin .from('transactions') - .select('*') + .select('*, merchants(name)') .order('created_at', { ascending: false }); - if (error) return []; + if (filters.merchant_id) { + query = query.eq('merchant_id', filters.merchant_id); + } + + if (filters.status) { + query = query.eq('status', filters.status); + } + + if (filters.q) { + // First, search for merchants matching the name to get their IDs + const { data: matchedMerchants } = await supabaseAdmin + .from('merchants') + .select('id') + .ilike('name', `%${filters.q}%`); + + const merchantIds = matchedMerchants?.map(m => m.id) || []; + + // Construct OR query parts + let orParts = [ + `stripe_pi_id.ilike.%${filters.q}%`, + `source_ref_id.ilike.%${filters.q}%`, + `customer_name.ilike.%${filters.q}%` + ]; + + if (merchantIds.length > 0) { + orParts.push(`merchant_id.in.(${merchantIds.join(',')})`); + } + + query = query.or(orParts.join(',')); + } + + const { data, error } = await query; + + if (error) { + console.error('Fetch error:', error); + return []; + } return data; } -export default async function TransactionsPage() { - const transactions = await getTransactions(); +export default async function TransactionsPage(props: { + searchParams: Promise<{ merchant_id?: string; q?: string; status?: string }>; +}) { + const searchParams = await props.searchParams; + const transactions = await getTransactions({ + merchant_id: searchParams.merchant_id, + q: searchParams.q, + status: searchParams.status + }); return (
{/* Search and Filters Header */}
-
- - -
- + +
+
+ ); +} diff --git a/app/merchant/[id]/(dashboard)/transactions/page.tsx b/app/merchant/[id]/(dashboard)/transactions/page.tsx new file mode 100644 index 0000000..03a08cb --- /dev/null +++ b/app/merchant/[id]/(dashboard)/transactions/page.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { supabaseAdmin } from '@/lib/supabase-admin'; +import { format } from 'date-fns'; +import { tr } from 'date-fns/locale'; +import { + CreditCard, + Search, + Filter, + Download, + Calendar +} from 'lucide-react'; +import Link from 'next/link'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; + +async function getMerchantTransactions(identifier: string) { + const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier); + + const query = supabaseAdmin + .from('merchants') + .select('id') + + if (isUUID) { + query.eq('id', identifier); + } else { + query.eq('short_id', identifier); + } + + const { data: merchant } = await query.single(); + if (!merchant) return null; + + const { data, error } = await supabaseAdmin + .from('transactions') + .select('*') + .eq('merchant_id', merchant.id) + .order('created_at', { ascending: false }); + + if (error) return null; + return data; +} + +export default async function MerchantTransactionsPage(props: { + params: Promise<{ id: string }>; +}) { + const resolvedParams = await props.params; + const identifier = resolvedParams.id; + const transactions = await getMerchantTransactions(identifier); + const cookieStore = await cookies(); + + if (!transactions) return
İşlemler yükleniyor...
; + + // Resolve merchant UUID briefly for auth check + const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier); + let resolvedId = identifier; + if (!isUUID) { + const { data: merchant } = await supabaseAdmin.from('merchants').select('id').eq('short_id', identifier).single(); + if (merchant) resolvedId = merchant.id; + } + + if (!cookieStore.get(`merchant_auth_${resolvedId}`)) { + redirect(`/merchant/${identifier}/login`); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

İşlem Listesi

+

Tüm ödeme hareketleri

+
+
+ +
+ +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + {transactions.map((t) => ( + + + + + + + + + ))} + +
İşlem NoMüşteri / ReferansTarihTutarDurumGateway
+
+ #{t.stripe_pi_id?.slice(-8).toUpperCase() || 'EXT-' + t.id.slice(0, 4)} + {t.id.slice(0, 8)} +
+
+
+ {t.customer_name || t.source_ref_id || 'SİSTEM'} + {t.customer_phone || 'İletişim Yok'} +
+
+
+ + {format(new Date(t.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })} +
+
+ {Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })} ₺ + + + {t.status === 'succeeded' ? 'Başarılı' : t.status === 'failed' ? 'Hatalı' : 'Bekliyor'} + + + + {t.provider || 'STRIPE'} + +
+ {transactions.length === 0 && ( +
+
+ +
+

Henüz bir işlem kaydedilmemiş

+
+ )} +
+
+ ); +} diff --git a/app/merchant/[id]/login/page.tsx b/app/merchant/[id]/login/page.tsx new file mode 100644 index 0000000..a08b55e --- /dev/null +++ b/app/merchant/[id]/login/page.tsx @@ -0,0 +1,103 @@ +'use client'; + +import React, { useState } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { ShieldCheck, ArrowRight, Lock, Building2 } from 'lucide-react'; + +export default function MerchantLoginPage() { + const [apiKey, setApiKey] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const router = useRouter(); + const params = useParams(); + const id = params.id as string; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + // We use an API route to verify the key and set a cookie + const response = await fetch(`/api/merchants/auth`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier: id, apiKey }) + }); + + const data = await response.json(); + + if (response.ok) { + router.push(`/merchant/${id}`); + router.refresh(); + } else { + setError(data.error || 'Geçersiz anahtar.'); + } + } catch (err) { + setError('Bir hata oluştu. Lütfen tekrar deneyin.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+ +
+

Firma Girişi

+

P2CGateway Güvenli Erişim Paneli

+
+ +
+
+

Yönetim anahtarınızı girin

+

Size özel tanımlanan API Secret Key ile giriş yapın.

+
+ +
+
+
+ + setApiKey(e.target.value)} + placeholder="••••••••••••••••" + className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-mono font-bold text-gray-900" + /> +
+ {error &&

{error}

} +
+ + +
+
+ +
+ +
+
+ +
+ + SSL 256-bit Uçtan Uca Şifreleme +
+
+ ); +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..051f00b --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,43 @@ +import Link from 'next/link'; +import { ArrowLeft, Search } from 'lucide-react'; + +export default function NotFound() { + return ( +
+
+ +
+ +

404

+
+

Sayfa Bulunamadı

+
+ +

+ Aradığınız sayfa taşınmış veya silinmiş olabilir. Kaybolmuş hissetmeyin, sizi ana sayfaya götürelim. +

+ +
+ + Ana Sayfaya Dön + + + Panele Git + +
+ +
+
+
+ P2CGateway Sistemleri Aktif +
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 632991d..d95ef86 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,91 +2,190 @@ import React, { useEffect, useState } from 'react'; import Link from 'next/link'; -import { ShieldCheck, CreditCard, LayoutDashboard, Zap } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { ShieldCheck, CreditCard, LayoutDashboard, Zap, Building2, ArrowRight } from 'lucide-react'; export default function Home() { const [randomAmount, setRandomAmount] = useState(150); const [refId, setRefId] = useState('DEMO-123'); const [mounted, setMounted] = useState(false); + const [merchantId, setMerchantId] = useState(''); + const [placeholderIndex, setPlaceholderIndex] = useState(0); + const router = useRouter(); + + const placeholders = ['P2C-X7R2B9', 'P2C-A1B2C3', 'MERCHANT-ID']; useEffect(() => { setMounted(true); - // Random amount between 50 and 5000 setRandomAmount(Math.floor(Math.random() * 4950) + 50); - // Random ref id setRefId(`DEMO-${Math.floor(Math.random() * 900) + 100}`); + + const interval = setInterval(() => { + setPlaceholderIndex((prev) => (prev + 1) % placeholders.length); + }, 3000); + return () => clearInterval(interval); }, []); - // Return a static version or null during SSR to avoid mismatch - // Or just use the state which will be '150' and 'DEMO-123' on server - // and then update on client. The mismatch happens because of Math.random() in JSX. + const handleMerchantLogin = (e: React.FormEvent) => { + e.preventDefault(); + if (merchantId.trim()) { + router.push(`/merchant/${merchantId.trim()}/login`); + } + }; return (
- {/* Hero Section */} -
-
- - v1.0.0 Yayında + {/* Top Mini Nav */} + + + {/* Hero Section */} +
+
+ + v1.2.0 Global Deployment +
+

+ Ödemelerinizi Tek Bir
Kanaldan Yönetin

-

- Stripe altyapısı ile projelerinize kolayca ödeme geçidi ekleyin. Merkezi yönetim paneli ile tüm işlemlerinizi tek bir yerden takip edin. +

+ Sınırları aşan bir ödeme deneyimi. Stripe'ın küresel gücünü, Nuvei'nin esnekliğini ve
+ kripto dünyasının özgürlüğünü tek bir entegrasyonla işinize katın.
+ Her işlemde maksimum dönüşüm, her saniyede tam güvenlik.

-
- - {mounted ? `Test Ödemesi Başlat (${randomAmount.toLocaleString('tr-TR')} ₺)` : 'Ödeme Sayfasını Test Et'} - - - Admin Panelini Gör - + +
+
+ + {mounted ? `Canlı Test: ${randomAmount.toLocaleString('tr-TR')} ₺ Ödeme Sayfası` : 'Ödeme Sayfasını Test Et'} + +
+ +
+ + {/* Merchant Portal Quick Access */} +
+
+ +

Firma Yönetim Portalı

+
+
+ setMerchantId(e.target.value)} + placeholder={`Örn: ${placeholders[placeholderIndex]}`} + className="flex-1 px-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-2xl outline-none transition-all font-bold text-sm text-gray-900 placeholder:text-gray-300" + /> + +
+

Kayıtlı firmanızın paneline erişmek için ID giriniz.

+
- {/* Features */} -
-
-
-
- -
-

Güvenli Altyapı

-

- Stripe Elements kullanarak kart bilgilerini asla sunucularınızda saklamazsınız. Tam güvenlik garantisi. -

-
-
-
- -
-

Dinamik Ödeme

-

- Herhangi bir URL parametresi ile ödeme başlatın. Projelerinize entegre etmek sadece bir dakika sürer. -

-
-
-
- -
-

Merkezi Takip

-

- Tüm projelerinizden gelen ödemeleri tek bir admin panelinden, anlık grafikler ve raporlarla izleyin. -

+ {/* Supported Gateways */} +
+
+

Desteklenen Altyapılar

+

Global Ödeme Çözümleri ile Tam Entegrasyon

+
+ {['Stripe', 'Cryptomus', 'Nuvei', 'PayKings', 'SecurionPay'].map((brand) => ( +
+ {brand} +
+ ))}
+ {/* Code Snippet Section */} +
+
+
+

Tek Satır Kodla Ödeme Almaya Başlayın

+

+ API dokümantasyonumuz sayesinde projelerinize saniyeler içinde ödeme kabiliyeti kazandırın. + Karmaşık backend süreçlerini biz halledelim, siz işinizi büyütün. +

+ + Dokümantasyonu İncele + +
+
+
+
+
+
+
+
+              {`// Ödeme Linki Oluşturun
+const checkoutUrl = "https://p2cgateway.com/checkout";
+const params = {
+  merchant_id: "m_92183",
+  amount: 250.00,
+  currency: "TRY",
+  ref_id: "ORDER_923",
+  callback_url: "https://siteniz.com/succes"
+};
+
+// Müşteriyi yönlendirin
+window.location.href = \`\${checkoutUrl}?\${new URLSearchParams(params)}\`;`}
+            
+
+
+
+ + {/* CTA Section */} +
+
+

İşinizi Bugün Büyütün

+

P2CGateway ile global pazarlara açılmak artık çok daha kolay.

+ + Hemen Ücretsiz Başlayın + +
+
+ {/* Footer */} -
-

© 2026 froyd Payment Platforms. Tüm hakları saklıdır.

+
); diff --git a/components/admin/AddMerchantModal.tsx b/components/admin/AddMerchantModal.tsx new file mode 100644 index 0000000..efe05ea --- /dev/null +++ b/components/admin/AddMerchantModal.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { X, Building2, Globe, CheckCircle2, Loader2 } from 'lucide-react'; + +interface AddMerchantModalProps { + isOpen: boolean; + onClose: () => void; +} + +export default function AddMerchantModal({ isOpen, onClose }: AddMerchantModalProps) { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [name, setName] = useState(''); + const [webhookUrl, setWebhookUrl] = useState(''); + const [success, setSuccess] = useState(false); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + const response = await fetch('/api/merchants', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, webhook_url: webhookUrl }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Firma eklenemedi.'); + } + + setSuccess(true); + setTimeout(() => { + onClose(); + setSuccess(false); + setName(''); + setWebhookUrl(''); + router.refresh(); + }, 2000); + } catch (err: any) { + console.error('Error adding merchant:', err); + alert(err.message || 'Firma eklenirken bir hata oluştu.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+
+
+
+
+ +
+

Yeni Firma Ekle

+
+ +
+ + {success ? ( +
+
+ +
+
+

Firma Eklendi!

+

Firma başarıyla kaydedildi, yönlendiriliyorsunuz...

+
+
+ ) : ( +
+
+ +
+ setName(e.target.value)} + className="w-full px-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300" + /> +
+
+ +
+ +
+ + setWebhookUrl(e.target.value)} + className="w-full pl-14 pr-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300" + /> +
+

Ödeme başarılı olduğunda sistemin bu adrese veri göndermesini istiyorsanız girin.

+
+ +
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/components/admin/AnalyticsBarChart.tsx b/components/admin/AnalyticsBarChart.tsx new file mode 100644 index 0000000..c5a1843 --- /dev/null +++ b/components/admin/AnalyticsBarChart.tsx @@ -0,0 +1,81 @@ +'use client'; + +import React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell +} from 'recharts'; + +interface AnalyticsBarChartProps { + data: { + label: string; + amount: number; + }[]; +} + +export default function AnalyticsBarChart({ data }: AnalyticsBarChartProps) { + return ( +
+ + + + + + } + cursor={{ fill: '#F8FAFC' }} + /> + + {data.map((entry, index) => ( + + ))} + + + +
+ ); +} + +function CustomTooltip({ active, payload, label }: any) { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {payload[0].value.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} ₺ +

+
+ ); + } + return null; +} diff --git a/components/admin/CustomerSearch.tsx b/components/admin/CustomerSearch.tsx new file mode 100644 index 0000000..dfee40b --- /dev/null +++ b/components/admin/CustomerSearch.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Search } from 'lucide-react'; + +export default function CustomerSearch() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || ''); + + useEffect(() => { + const currentQ = searchParams.get('q') || ''; + if (searchTerm === currentQ) return; + + const delayDebounceFn = setTimeout(() => { + const params = new URLSearchParams(searchParams.toString()); + if (searchTerm) { + params.set('q', searchTerm); + } else { + params.delete('q'); + } + router.push(`/admin/customers?${params.toString()}`); + }, 500); + + return () => clearTimeout(delayDebounceFn); + }, [searchTerm, searchParams, router]); + + return ( +
+ + setSearchTerm(e.target.value)} + className="w-full pl-16 pr-6 py-5 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300" + /> +
+ ); +} diff --git a/components/admin/QueryRangeSelector.tsx b/components/admin/QueryRangeSelector.tsx new file mode 100644 index 0000000..a3cc8b2 --- /dev/null +++ b/components/admin/QueryRangeSelector.tsx @@ -0,0 +1,29 @@ +'use client'; + +import React from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; + +export default function QueryRangeSelector() { + const router = useRouter(); + const searchParams = useSearchParams(); + const currentRange = searchParams.get('range') || '30'; + + const handleRangeChange = (e: React.ChangeEvent) => { + const newRange = e.target.value; + const params = new URLSearchParams(searchParams.toString()); + params.set('range', newRange); + router.push(`/admin?${params.toString()}`); + }; + + return ( + + ); +} diff --git a/components/admin/TransactionChart.tsx b/components/admin/TransactionChart.tsx new file mode 100644 index 0000000..8a1ac4b --- /dev/null +++ b/components/admin/TransactionChart.tsx @@ -0,0 +1,87 @@ +'use client'; + +import React from 'react'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer +} from 'recharts'; + +interface TransactionChartProps { + data: { + displayDate: string; + amount: number; + }[]; +} + +export default function TransactionChart({ data }: TransactionChartProps) { + return ( +
+ + + + + + + + + + + + } + cursor={{ stroke: '#2563EB', strokeWidth: 1, strokeDasharray: '4 4' }} + /> + + + +
+ ); +} + +function CustomTooltip({ active, payload, label }: any) { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ {payload[0].value.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} ₺ +

+
+ ); + } + return null; +} diff --git a/components/admin/TransactionSearch.tsx b/components/admin/TransactionSearch.tsx new file mode 100644 index 0000000..e3b2c52 --- /dev/null +++ b/components/admin/TransactionSearch.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Search } from 'lucide-react'; + +export default function TransactionSearch() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || ''); + + useEffect(() => { + const currentQ = searchParams.get('q') || ''; + if (searchTerm === currentQ) return; + + const delayDebounceFn = setTimeout(() => { + const params = new URLSearchParams(searchParams.toString()); + if (searchTerm) { + params.set('q', searchTerm); + } else { + params.delete('q'); + } + router.push(`/admin/transactions?${params.toString()}`); + }, 500); + + return () => clearTimeout(delayDebounceFn); + }, [searchTerm, searchParams, router]); + + return ( +
+ + setSearchTerm(e.target.value)} + className="w-full pl-12 pr-6 py-3 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300" + /> +
+ ); +} diff --git a/components/admin/TransactionStatusFilter.tsx b/components/admin/TransactionStatusFilter.tsx new file mode 100644 index 0000000..5a29142 --- /dev/null +++ b/components/admin/TransactionStatusFilter.tsx @@ -0,0 +1,40 @@ +'use client'; + +import React from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Filter } from 'lucide-react'; + +export default function TransactionStatusFilter() { + const router = useRouter(); + const searchParams = useSearchParams(); + const currentStatus = searchParams.get('status') || ''; + + const handleStatusChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value === currentStatus) return; + + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set('status', value); + } else { + params.delete('status'); + } + router.push(`/admin/transactions?${params.toString()}`); + }; + + return ( +
+ + +
+ ); +} diff --git a/components/merchant/MerchantSidebar.tsx b/components/merchant/MerchantSidebar.tsx new file mode 100644 index 0000000..8fcb074 --- /dev/null +++ b/components/merchant/MerchantSidebar.tsx @@ -0,0 +1,91 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; +import { + LayoutDashboard, + CreditCard, + ExternalLink, + Terminal, + Building2, + ShieldCheck, + LogOut +} from 'lucide-react'; + +export default function MerchantSidebar({ merchantId }: { merchantId: string }) { + const pathname = usePathname(); + const router = useRouter(); + + const handleLogout = async () => { + try { + await fetch('/api/merchants/logout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier: merchantId }) + }); + router.push('/'); + router.refresh(); + } catch (err) { + console.error('Logout failed'); + } + }; + + const navItems = [ + { label: 'Panel', icon: LayoutDashboard, href: `/merchant/${merchantId}` }, + { label: 'İşlemler', icon: CreditCard, href: `/merchant/${merchantId}/transactions` }, + { label: 'Entegrasyon', icon: Terminal, href: `/merchant/${merchantId}/integration` }, + ]; + + return ( + + ); +} diff --git a/docs/db_schema.sql b/docs/db_schema.sql index ec2cdba..81720fc 100644 --- a/docs/db_schema.sql +++ b/docs/db_schema.sql @@ -5,8 +5,14 @@ CREATE TABLE admin_users ( created_at TIMESTAMPTZ DEFAULT NOW() ); --- Register initial admin (User should replace this or add via dashboard) --- INSERT INTO admin_users (email) VALUES ('your-email@example.com'); +-- Merchants (Firms) table +CREATE TABLE merchants ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + api_key TEXT UNIQUE DEFAULT encode(gen_random_bytes(32), 'hex'), + webhook_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); -- Transactions table CREATE TABLE transactions ( @@ -20,18 +26,27 @@ CREATE TABLE transactions ( customer_name TEXT, customer_phone TEXT, callback_url TEXT, + merchant_id UUID REFERENCES merchants(id), metadata JSONB DEFAULT '{}'::jsonb ); -- Enable RLS ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; +ALTER TABLE merchants ENABLE ROW LEVEL SECURITY; -- Create policy for admins to read all CREATE POLICY "Admins can read all transactions" ON transactions FOR SELECT USING (auth.jwt() ->> 'email' IN (SELECT email FROM admin_users)); +CREATE POLICY "Admins can manage merchants" ON merchants + USING (auth.jwt() ->> 'email' IN (SELECT email FROM admin_users)); + -- Create policy for service role to manage all -CREATE POLICY "Service role can manage all" ON transactions +CREATE POLICY "Service role can manage all transactions" ON transactions + USING (true) + WITH CHECK (true); + +CREATE POLICY "Service role can manage all merchants" ON merchants USING (true) WITH CHECK (true); diff --git a/docs/update_schema_merchants.sql b/docs/update_schema_merchants.sql new file mode 100644 index 0000000..16f00fd --- /dev/null +++ b/docs/update_schema_merchants.sql @@ -0,0 +1,17 @@ +-- Create merchants table +CREATE TABLE merchants ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + api_key TEXT UNIQUE DEFAULT encode(gen_random_bytes(32), 'hex'), + webhook_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Add merchant_id to transactions +ALTER TABLE transactions ADD COLUMN merchant_id UUID REFERENCES merchants(id); + +-- Update RLS for transactions to support merchants (future proofing) +-- For now, we'll just keep the admin policy as is, but we'll add more later. + +-- Optional: Add index for performance +CREATE INDEX idx_transactions_merchant_id ON transactions(merchant_id); diff --git a/docs/update_schema_providers.sql b/docs/update_schema_providers.sql new file mode 100644 index 0000000..0b21fca --- /dev/null +++ b/docs/update_schema_providers.sql @@ -0,0 +1,11 @@ +-- Add payment provider configuration to merchants table +ALTER TABLE merchants ADD COLUMN payment_provider TEXT NOT NULL DEFAULT 'stripe'; +ALTER TABLE merchants ADD COLUMN provider_config JSONB DEFAULT '{}'::jsonb; + +-- Add provider info to transactions to track which one was used +ALTER TABLE transactions ADD COLUMN provider TEXT NOT NULL DEFAULT 'stripe'; +ALTER TABLE transactions ADD COLUMN provider_tx_id TEXT; +ALTER TABLE transactions ADD COLUMN provider_status TEXT; + +-- Update status constraint if needed (ours was already quite flexible, but let's be sure) +-- Currently: CHECK (status IN ('pending', 'succeeded', 'failed')) diff --git a/docs/update_schema_short_ids.sql b/docs/update_schema_short_ids.sql new file mode 100644 index 0000000..5baee4a --- /dev/null +++ b/docs/update_schema_short_ids.sql @@ -0,0 +1,25 @@ +-- Add short_id column to merchants +ALTER TABLE merchants ADD COLUMN IF NOT EXISTS short_id TEXT UNIQUE; + +-- Function to generate a random short ID +CREATE OR REPLACE FUNCTION generate_short_id() RETURNS TEXT AS $$ +DECLARE + chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + result TEXT := ''; + i INTEGER := 0; +BEGIN + FOR i IN 1..8 LOOP + result := result || substr(chars, floor(random() * length(chars) + 1)::integer, 1); + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql; + +-- Update existing merchants with a short_id +UPDATE merchants SET short_id = generate_short_id() WHERE short_id IS NULL; + +-- Make short_id required for further inserts +-- ALTER TABLE merchants ALTER COLUMN short_id SET NOT NULL; -- Can do this after update + +-- Add owner_id to merchants to link with Supabase Auth users +ALTER TABLE merchants ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES auth.users(id); diff --git a/lib/payment-providers.ts b/lib/payment-providers.ts new file mode 100644 index 0000000..09dabd8 --- /dev/null +++ b/lib/payment-providers.ts @@ -0,0 +1,78 @@ +import { stripe } from './stripe'; + +export interface PaymentIntentOptions { + amount: number; + currency: string; + merchantId: string; + refId?: string; + customerName?: string; + customerPhone?: string; + callbackUrl?: string; + providerConfig?: any; +} + +export interface PaymentIntentResponse { + clientSecret: string; + providerTxId: string; + nextAction?: 'redirect' | 'iframe' | 'none'; + redirectUrl?: string; +} + +export const PaymentProviderFactory = { + async createIntent(provider: string, options: PaymentIntentOptions): Promise { + switch (provider.toLowerCase()) { + case 'stripe': + return this.handleStripe(options); + case 'cryptomus': + return this.handleCryptomus(options); + case 'nuvei': + case 'paykings': + case 'highriskpay': + case 'paymentcloud': + case 'securionpay': + // For now, these will use mock or a generic handler + return this.handleGeneric(provider, options); + default: + return this.handleStripe(options); + } + }, + + async handleStripe(options: PaymentIntentOptions): Promise { + const paymentIntent = await stripe.paymentIntents.create({ + amount: Math.round(options.amount * 100), + currency: options.currency.toLowerCase(), + metadata: { + ref_id: options.refId || '', + merchant_id: options.merchantId, + customer_name: options.customerName || '', + customer_phone: options.customerPhone || '', + }, + }); + + return { + clientSecret: paymentIntent.client_secret!, + providerTxId: paymentIntent.id + }; + }, + + async handleCryptomus(options: PaymentIntentOptions): Promise { + // Mock implementation for Cryptomus - real implementation would call their API + // Cryptomus usually returns a payment URL for redirect + const mockTxId = 'crypt_' + Math.random().toString(36).substring(7); + return { + clientSecret: mockTxId, + providerTxId: mockTxId, + nextAction: 'redirect', + redirectUrl: `https://cryptomus.com/pay/${mockTxId}` + }; + }, + + async handleGeneric(provider: string, options: PaymentIntentOptions): Promise { + // Mock generic handler for other providers + const mockTxId = `${provider.slice(0, 3)}_${Math.random().toString(36).substring(7)}`; + return { + clientSecret: `mock_secret_${mockTxId}`, + providerTxId: mockTxId + }; + } +}; diff --git a/lib/supabase-admin.ts b/lib/supabase-admin.ts new file mode 100644 index 0000000..7f61133 --- /dev/null +++ b/lib/supabase-admin.ts @@ -0,0 +1,9 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + +// This should ONLY be used in Server Components or API Routes +export const supabaseAdmin = createClient( + supabaseUrl, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); diff --git a/lib/supabase.ts b/lib/supabase.ts index 81252cc..719ad55 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -4,8 +4,3 @@ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; export const supabase = createClient(supabaseUrl, supabaseAnonKey); - -export const supabaseAdmin = createClient( - supabaseUrl, - process.env.SUPABASE_SERVICE_ROLE_KEY! -); diff --git a/package-lock.json b/package-lock.json index c2fd038..ded5875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "next": "16.1.1", "react": "19.2.3", "react-dom": "19.2.3", + "recharts": "^3.6.0", "stripe": "^20.1.2", "tailwind-merge": "^3.4.0" }, @@ -1236,6 +1237,42 @@ "node": ">=12.4.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1243,6 +1280,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@stripe/react-stripe-js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.4.1.tgz", @@ -1651,6 +1700,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1677,7 +1789,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.29.tgz", "integrity": "sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1692,7 +1803,7 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1709,6 +1820,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -2795,9 +2912,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2887,6 +3125,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3169,6 +3413,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3618,6 +3872,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4077,6 +4337,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4119,6 +4389,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5600,7 +5879,78 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", + "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -5646,6 +5996,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -6261,6 +6617,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6599,6 +6961,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 401a928..c9db6d0 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "next": "16.1.1", "react": "19.2.3", "react-dom": "19.2.3", + "recharts": "^3.6.0", "stripe": "^20.1.2", "tailwind-merge": "^3.4.0" }, diff --git a/middleware.ts b/proxy.ts similarity index 82% rename from middleware.ts rename to proxy.ts index f453503..bbb3f8c 100644 --- a/middleware.ts +++ b/proxy.ts @@ -1,7 +1,7 @@ import { type NextRequest } from 'next/server' -import { updateSession } from '@/utils/supabase/middleware' +import { updateSession } from '@/utils/supabase/proxy' -export async function middleware(request: NextRequest) { +export async function proxy(request: NextRequest) { return await updateSession(request) } diff --git a/utils/supabase/middleware.ts b/utils/supabase/proxy.ts similarity index 100% rename from utils/supabase/middleware.ts rename to utils/supabase/proxy.ts