first commit

This commit is contained in:
mstfyldz
2026-01-18 16:48:15 +03:00
parent 68ba54fa4f
commit af09543374
29 changed files with 2666 additions and 82 deletions

View File

@@ -0,0 +1,177 @@
import React from 'react';
import {
BarChart3,
TrendingUp,
ArrowUpRight,
ArrowDownRight,
Globe,
Monitor,
Smartphone,
Calendar
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
import { format, subDays } from 'date-fns';
import { tr } from 'date-fns/locale';
async function getAnalyticsData() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: true });
if (error || !transactions) return null;
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
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);
return {
date: d.toISOString().split('T')[0],
label: format(d, 'd MMM', { locale: tr }),
amount: 0
};
});
successfulTransactions.forEach(t => {
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
const periodMatch = last12Periods.find(p => p.date === dateStr);
if (periodMatch) {
periodMatch.amount += Number(t.amount);
}
});
return {
totalRevenue,
avgOrderValue,
chartData: last12Periods,
totalCount: transactions.length,
successCount: successfulTransactions.length,
};
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
if (!data) return <div className="p-10 font-black text-gray-400 uppercase tracking-[0.2em] animate-pulse">Veriler hazırlanıyor...</div>;
const metrics = [
{ label: 'Dönüşüm Oranı', value: '3.24%', trend: '+0.8%', positive: true }, // Mocked for now
{ label: 'Ort. Sipariş Tutarı', value: `${data.avgOrderValue.toLocaleString('tr-TR', { maximumFractionDigits: 2 })} ₺`, trend: '+12%', positive: true },
{ label: 'Başarılı İşlem', value: data.successCount.toString(), trend: '+24%', positive: true },
{ label: 'İşlem Başarısı', value: `${((data.successCount / (data.totalCount || 1)) * 100).toFixed(1)}%`, trend: '-0.2%', positive: false },
];
const maxChartAmount = Math.max(...data.chartData.map(d => d.amount), 100);
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Analizler</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Sistem performans verileri</p>
</div>
<div className="flex items-center gap-4">
<button className="flex items-center gap-3 px-6 py-4 bg-white border border-gray-100 rounded-2xl text-xs font-black text-gray-600 hover:bg-gray-50 transition uppercase tracking-widest">
<Calendar size={18} className="text-gray-300" />
Son 30 Gün
</button>
</div>
</div>
{/* Main Metrics */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{metrics.map((item, i) => (
<div key={i} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm space-y-4">
<p className="text-[10px] text-gray-400 font-black uppercase tracking-[0.2em]">{item.label}</p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-black text-gray-900 leading-none">{item.value}</h3>
<div className={`flex items-center gap-1 text-[10px] font-black uppercase tracking-tighter ${item.positive ? 'text-emerald-500' : 'text-red-500'}`}>
{item.positive ? <ArrowUpRight size={14} /> : <ArrowDownRight size={14} />}
{item.trend}
</div>
</div>
</div>
))}
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Performance Chart */}
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-10 text-sans tracking-tight">
<div className="flex justify-between items-center">
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Ciro Trendi (12 Gün)</h3>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Gerçekleşen</span>
</div>
</div>
</div>
<div className="h-72 flex items-end justify-between gap-4">
{data.chartData.map((d, i) => {
const h = (d.amount / maxChartAmount) * 90 + 5; // 5% to 95%
return (
<div key={i} className="flex-1 group relative h-full flex flex-col justify-end">
<div
className="w-full bg-blue-500 rounded-t-xl transition-all duration-500 group-hover:bg-blue-600 cursor-pointer relative"
style={{ height: `${h}%` }}
>
<div className="absolute -top-12 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-[10px] font-black py-2 px-3 rounded-lg opacity-0 group-hover:opacity-100 transition shadow-xl pointer-events-none whitespace-nowrap z-20">
{d.amount.toLocaleString('tr-TR')}
</div>
</div>
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 text-[9px] font-black text-gray-300 uppercase tracking-tighter text-center">
{d.label}
</div>
</div>
);
})}
</div>
</div>
{/* Breakdown Grid */}
<div className="grid grid-cols-1 gap-8 text-sans tracking-tight">
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm">
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight mb-8">Cihaz Dağılımı</h3>
<div className="space-y-8">
{[
{ 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) => (
<div key={i} className="space-y-3">
<div className="flex items-center justify-between text-xs font-black text-gray-900 uppercase tracking-widest">
<div className="flex items-center gap-3">
<item.icon size={18} className="text-gray-300" />
<span>{item.label}</span>
</div>
<span>{item.value}</span>
</div>
<div className="h-3 w-full bg-gray-50 rounded-full overflow-hidden border border-gray-100">
<div className={`h-full ${item.color} rounded-full`} style={{ width: item.width }}></div>
</div>
</div>
))}
</div>
</div>
<div className="bg-[#2563EB] p-10 rounded-[40px] shadow-2xl shadow-blue-200 text-white relative overflow-hidden group">
<div className="relative z-10 space-y-6">
<h3 className="text-2xl font-black leading-tight">Analizleriniz hazır! <br /> Bu ay başarılı bir grafik çiziyorsunuz.</h3>
<button className="px-8 py-4 bg-white text-blue-600 rounded-2xl font-black text-sm hover:scale-105 transition active:scale-95 shadow-xl uppercase tracking-widest">
Akıllı İpuçlarını
</button>
</div>
<BarChart3 className="absolute -bottom-10 -right-10 w-64 h-64 text-white/10 rotate-12 group-hover:rotate-0 transition-transform duration-700" />
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,179 @@
import React from 'react';
import {
Users,
Search,
Plus,
Mail,
Phone,
MoreHorizontal,
ArrowUpRight
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
async function getCustomers() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*');
if (error || !transactions) return null;
// Group transactions by name or phone
const customerMap = new Map();
transactions.forEach(t => {
const key = t.customer_name || t.customer_phone || 'Unknown';
if (!customerMap.has(key)) {
customerMap.set(key, {
id: t.id,
name: t.customer_name || 'İsimsiz Müşteri',
phone: t.customer_phone || 'Telefon Yok',
spent: 0,
orders: 0,
status: 'New'
});
}
const c = customerMap.get(key);
c.orders += 1;
if (t.status === 'succeeded') {
c.spent += Number(t.amount);
}
});
const customers = Array.from(customerMap.values()).map(c => {
if (c.orders > 5) c.status = 'High Value';
else if (c.orders > 1) c.status = 'Active';
return c;
});
return customers;
}
export default async function CustomersPage() {
const customers = await getCustomers();
if (!customers) return <div className="p-10 font-black text-gray-400 uppercase tracking-widest animate-pulse">Müşteriler yükleniyor...</div>;
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Müşteriler</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Müşteri portföyünüzü yönetin</p>
</div>
<button className="flex items-center justify-center gap-3 px-8 py-4 bg-[#2563EB] text-white rounded-2xl font-black shadow-xl shadow-blue-100 hover:bg-blue-700 transition active:scale-95 uppercase text-xs tracking-widest">
<Plus size={18} />
Yeni Müşteri Ekle
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8">
<div className="w-16 h-16 bg-blue-50 rounded-[20px] flex items-center justify-center text-blue-600">
<Users size={32} />
</div>
<div>
<p className="text-3xl font-black text-gray-900">{customers.length.toLocaleString('tr-TR')}</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Toplam Müşteri</p>
</div>
</div>
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8">
<div className="w-16 h-16 bg-emerald-50 rounded-[20px] flex items-center justify-center text-emerald-600">
<ArrowUpRight size={32} />
</div>
<div>
<p className="text-3xl font-black text-gray-900">Gerçek</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Canlı Veri</p>
</div>
</div>
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8">
<div className="w-16 h-16 bg-orange-50 rounded-[20px] flex items-center justify-center text-orange-600">
<Phone size={32} />
</div>
<div>
<p className="text-3xl font-black text-gray-900">{customers.filter(c => c.phone !== 'Telefon Yok').length}</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Telefon Kayıtlı</p>
</div>
</div>
</div>
{/* List */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden">
<div className="p-8 border-b border-gray-50 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="text"
placeholder="İsim veya telefon ile ara..."
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"
/>
</div>
<button className="text-blue-600 text-xs font-black uppercase tracking-widest hover:underline decoration-2 underline-offset-4 px-6 py-4">
Görünümü Filtrele
</button>
</div>
<div className="overflow-x-auto text-sans tracking-tight">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/30 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
<th className="px-10 py-8">Müşteri Bilgileri</th>
<th className="px-10 py-8">Segment</th>
<th className="px-10 py-8">Sipariş</th>
<th className="px-10 py-8">Toplam Harcama</th>
<th className="px-10 py-8 text-right">Aksiyonlar</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{customers.map((customer, i) => (
<tr key={i} className="group hover:bg-gray-50/50 transition-colors">
<td className="px-10 py-10">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-gray-100 rounded-2xl flex items-center justify-center text-gray-400 font-black text-sm uppercase tracking-tighter">
{customer.name.slice(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-sm font-black text-gray-900 uppercase tracking-tight">{customer.name}</span>
<span className="text-[10px] text-gray-400 font-bold mt-1 tracking-widest">{customer.phone}</span>
</div>
</div>
</td>
<td className="px-10 py-10">
<span className={`inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest ${customer.status === 'Active' ? 'bg-emerald-50 text-emerald-600' :
customer.status === 'High Value' ? 'bg-blue-50 text-blue-600' :
customer.status === 'New' ? 'bg-indigo-50 text-indigo-600' :
'bg-gray-50 text-gray-400'
}`}>
{customer.status === 'Active' ? 'Aktif' :
customer.status === 'High Value' ? 'VIP' :
customer.status === 'New' ? 'Yeni Üye' : 'İnaktif'}
</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">{customer.orders}</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">
{customer.spent.toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
</span>
</td>
<td className="px-10 py-10 text-right">
<div className="flex items-center justify-end gap-3">
<button className="p-3 bg-gray-50 text-gray-400 rounded-xl hover:text-blue-600 hover:bg-blue-50 transition">
<Phone size={18} />
</button>
<button className="p-3 bg-gray-50 text-gray-400 rounded-xl hover:text-gray-900 hover:bg-gray-100 transition">
<MoreHorizontal size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

137
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,137 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
LayoutDashboard,
CreditCard,
Users,
BarChart3,
Settings,
LogOut,
Search,
Bell,
MessageSquare,
ChevronDown,
Wallet
} from 'lucide-react';
import Image from 'next/image';
import { createClient } from '@/utils/supabase/client'; // Assuming a client-side Supabase client utility
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const supabase = createClient();
const handleSignOut = async () => {
await supabase.auth.signOut();
router.push('/login');
router.refresh();
};
const navItems = [
{ label: 'Genel Bakış', icon: LayoutDashboard, href: '/admin' },
{ label: 'İşlemler', icon: CreditCard, href: '/admin/transactions' },
{ label: 'Müşteriler', icon: Users, href: '/admin/customers' },
{ label: 'Analizler', icon: BarChart3, href: '/admin/analytics' },
{ label: 'Ayarlar', icon: Settings, href: '/admin/settings' },
];
return (
<div className="flex h-screen bg-[#F8FAFC]">
{/* Sidebar */}
<aside className="w-72 bg-white border-r border-gray-100 flex flex-col shrink-0">
<div className="p-8 flex items-center gap-4">
<div className="w-12 h-12 bg-[#2563EB] rounded-xl flex items-center justify-center text-white shadow-lg shadow-blue-100">
<Wallet size={24} />
</div>
<div>
<h1 className="font-black text-gray-900 leading-tight">froyd Admin</h1>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Yönetim Paneli</p>
</div>
</div>
<nav className="flex-1 px-4 mt-4 space-y-2">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.label}
href={item.label === 'Müşteriler' || item.label === 'Analizler' || item.label === 'Ayarlar' ? (item.href === pathname ? item.href : item.href) : item.href}
className={`flex items-center gap-4 px-6 py-4 rounded-2xl text-sm font-bold transition-all duration-200 group ${isActive
? 'bg-blue-50 text-blue-600'
: 'text-gray-400 hover:text-gray-900 hover:bg-gray-50'
}`}
>
<item.icon size={22} className={isActive ? 'text-blue-600' : 'text-gray-300 group-hover:text-gray-500'} />
{item.label}
</Link>
);
})}
</nav>
<div className="p-6 border-t border-gray-50">
<button
onClick={handleSignOut}
className="flex items-center gap-4 px-6 py-4 text-sm font-bold text-gray-400 hover:text-red-500 hover:bg-red-50 w-full rounded-2xl transition-all duration-200 group"
>
<LogOut size={22} className="text-gray-300 group-hover:text-red-500" />
Çıkış Yap
</button>
</div>
</aside>
{/* Main Container */}
<div className="flex-1 flex flex-col min-w-0">
{/* Top Bar */}
<header className="h-24 bg-white border-b border-gray-100 flex items-center justify-between px-10 gap-8">
<div className="flex-1 flex items-center">
<h2 className="text-xl font-black text-gray-900 mr-8 shrink-0">Yönetim Paneli</h2>
<div className="relative max-w-md w-full">
<Search className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="text"
placeholder="İşlemlerde ara..."
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300"
/>
</div>
</div>
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<button className="relative p-2 text-gray-400 hover:text-blue-600 transition">
<Bell size={24} />
<span className="absolute top-2 right-2 w-2 h-2 bg-blue-600 rounded-full border-2 border-white"></span>
</button>
<button className="p-2 text-gray-400 hover:text-blue-600 transition">
<MessageSquare size={24} />
</button>
</div>
<div className="flex items-center gap-4 pl-8 border-l border-gray-100">
<div className="text-right">
<p className="text-sm font-black text-gray-900 leading-none">Admin</p>
<p className="text-[10px] text-blue-600 font-bold uppercase tracking-wider mt-1">Süper Admin</p>
</div>
<div className="relative w-12 h-12 rounded-2xl bg-orange-100 overflow-hidden ring-4 ring-orange-50">
<div className="absolute inset-0 flex items-center justify-center text-orange-500 font-bold font-mono">AD</div>
{/* Fallback avatar if needed */}
</div>
<ChevronDown size={18} className="text-gray-300" />
</div>
</div>
</header>
{/* Content Area */}
<main className="flex-1 overflow-y-auto p-10">
{children}
</main>
</div>
</div>
);
}

327
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,327 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase';
import {
TrendingUp,
TrendingDown,
Users,
Wallet,
ClipboardList,
CheckCircle2,
} from 'lucide-react';
import { format } from 'date-fns';
import { tr } from 'date-fns/locale';
import Link from 'next/link';
async function getStats() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: false });
if (error || !transactions) return null;
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
const successfulCount = successfulTransactions.length;
const pendingCount = transactions.filter(t => t.status === 'pending').length;
const totalCount = transactions.length;
const successRate = totalCount > 0 ? (successfulCount / totalCount) * 100 : 0;
// Calculate unique customers
const uniqueCustomers = new Set(
transactions
.filter(t => t.customer_name || t.customer_phone)
.map(t => t.customer_name || t.customer_phone)
).size;
// Last 30 days chart data
const last30Days = Array.from({ length: 30 }, (_, i) => {
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - (29 - i));
return {
date: d.toISOString().split('T')[0],
displayDate: format(d, 'd MMM', { locale: tr }),
amount: 0
};
});
successfulTransactions.forEach(t => {
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
const dayMatch = last30Days.find(d => d.date === dateStr);
if (dayMatch) {
dayMatch.amount += Number(t.amount);
}
});
return {
transactions,
totalRevenue,
successfulCount,
pendingCount,
successRate,
totalCount,
uniqueCustomers,
chartData: last30Days
};
}
export default async function AdminDashboard() {
const stats = await getStats();
if (!stats) {
return <div className="p-10 font-bold text-gray-500 font-sans tracking-tight uppercase">Henüz bir işlem verisi bulunamadı.</div>;
}
const recentTransactions = stats.transactions.slice(0, 5);
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Top Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Total Revenue */}
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
<div className="flex justify-between items-start">
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Toplam Ciro</p>
<div className="p-3 bg-blue-50 rounded-xl text-blue-600">
<Wallet size={20} />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900">{stats.totalRevenue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} </h3>
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
<TrendingUp size={14} />
<span>Sistem Aktif <span className="text-gray-400 font-medium lowercase">gerçek zamanlı veri</span></span>
</div>
</div>
</div>
{/* Total Customers */}
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
<div className="flex justify-between items-start">
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Toplam Müşteri</p>
<div className="p-3 bg-indigo-50 rounded-xl text-indigo-600">
<Users size={20} />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900">{stats.uniqueCustomers.toLocaleString('tr-TR')}</h3>
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
<TrendingUp size={14} />
<span>{stats.totalCount} <span className="text-gray-400 font-medium lowercase">toplam işlem kaydı</span></span>
</div>
</div>
</div>
{/* Pending Payments */}
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
<div className="flex justify-between items-start">
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Bekleyen Ödemeler</p>
<div className="p-3 bg-orange-50 rounded-xl text-orange-600">
<ClipboardList size={20} />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900">{stats.pendingCount}</h3>
<div className="flex items-center gap-1 mt-2 text-orange-500 font-bold text-xs uppercase tracking-tighter">
<ClipboardList size={14} />
<span>İşlem Bekliyor <span className="text-gray-400 font-medium lowercase">onay aşamasında</span></span>
</div>
</div>
</div>
{/* Success Rate */}
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
<div className="flex justify-between items-start">
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Başarı Oranı</p>
<div className="p-3 bg-emerald-50 rounded-xl text-emerald-600">
<CheckCircle2 size={20} />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900">{stats.successRate.toFixed(1)}%</h3>
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
<TrendingUp size={14} />
<span>Optimized <span className="text-gray-400 font-medium lowercase">ödeme dönüşüm oranı</span></span>
</div>
</div>
</div>
</div>
{/* Middle Section: Charts */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Transaction Volume Line Chart */}
<div className="lg:col-span-2 bg-white p-8 rounded-3xl border border-gray-100 shadow-sm">
<div className="flex justify-between items-center mb-10">
<div>
<h3 className="text-lg font-black text-gray-900 leading-none">İşlem Hacmi</h3>
<p className="text-xs text-gray-400 font-bold uppercase tracking-wider mt-2">Son 30 günlük toplam hacim</p>
</div>
<select className="bg-gray-50 border-none rounded-xl text-[10px] font-black uppercase tracking-widest px-4 py-2 outline-none">
<option>Son 30 Gün</option>
<option>Son 7 Gün</option>
</select>
</div>
<div className="h-64 relative flex items-end justify-between px-4 pb-12">
{/* 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 (
<svg
viewBox="0 0 100 100"
className="absolute inset-0 w-full h-full text-blue-500 overflow-visible px-4 pt-10 pb-12"
preserveAspectRatio="none"
>
<path
d={dLine}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d={dArea}
fill="url(#chartGradient)"
stroke="none"
/>
<defs>
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="currentColor" stopOpacity="0.2" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
})()}
<div className="absolute inset-x-0 bottom-0 flex justify-between text-[10px] font-bold text-gray-300 uppercase px-8 pb-4 border-t border-gray-50 pt-4">
<span>{stats.chartData[0].displayDate}</span>
<span>{stats.chartData[10].displayDate}</span>
<span>{stats.chartData[20].displayDate}</span>
<span>Bugün</span>
</div>
</div>
</div>
{/* Revenue by Source Donut Chart */}
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm flex flex-col">
<h3 className="text-lg font-black text-gray-900 leading-none mb-10 uppercase tracking-tight">Kaynak Bazlı Ciro</h3>
<div className="flex-1 flex flex-col items-center justify-center space-y-8">
<div className="relative w-48 h-48">
<svg className="w-full h-full -rotate-90">
<circle cx="96" cy="96" r="80" stroke="#F1F5F9" strokeWidth="24" fill="none" />
<circle cx="96" cy="96" r="80" stroke="#2563EB" strokeWidth="24" fill="none" strokeDasharray="502" strokeDashoffset="200" strokeLinecap="round" />
<circle cx="96" cy="96" r="80" stroke="#60A5FA" strokeWidth="24" fill="none" strokeDasharray="502" strokeDashoffset="400" strokeLinecap="round" />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center text-sans">
<p className="text-2xl font-black text-gray-900 truncate max-w-[120px] text-center">{stats.totalRevenue.toLocaleString('tr-TR', { maximumFractionDigits: 0 })} </p>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Toplam Ciro</p>
</div>
</div>
<div className="grid grid-cols-2 gap-x-8 gap-y-4 w-full px-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-600"></div>
<span className="text-xs font-bold text-gray-900">Kart (60%)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
<span className="text-xs font-bold text-gray-900">Havale (20%)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-gray-200"></div>
<span className="text-xs font-bold text-gray-900">Cüzdan (15%)</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-gray-100"></div>
<span className="text-xs font-bold text-gray-400 text-center uppercase tracking-tighter">Diğer (5%)</span>
</div>
</div>
</div>
</div>
</div>
{/* Bottom Section: Recent Transactions Table */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden text-sans tracking-tight">
<div className="p-8 border-b border-gray-50 flex justify-between items-center text-sans tracking-tight">
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tight">Son İşlemler</h2>
<Link href="/admin/transactions" className="text-blue-600 text-xs font-black uppercase tracking-widest hover:underline decoration-2 underline-offset-4">
Tümünü Gör
</Link>
</div>
<div className="overflow-x-auto text-sans tracking-tight">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/30 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
<th className="px-10 py-6">İşlem ID</th>
<th className="px-10 py-6">Müşteri / Ref</th>
<th className="px-10 py-6">Tarih</th>
<th className="px-10 py-6 text-right pr-20">Tutar</th>
<th className="px-10 py-6 text-center">Durum</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{recentTransactions.map((t) => (
<tr key={t.id} className="group hover:bg-gray-50/50 transition-colors">
<td className="px-10 py-8">
<div className="flex flex-col">
<span className="text-sm font-black text-gray-900">#{t.stripe_pi_id.slice(-8).toUpperCase()}</span>
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider mt-1">{t.id.slice(0, 8)}</span>
</div>
</td>
<td className="px-10 py-8">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600 font-black text-xs uppercase tracking-tighter">
{t.customer_name ? t.customer_name.slice(0, 2).toUpperCase() : (t.source_ref_id ? t.source_ref_id.slice(0, 2).toUpperCase() : 'PI')}
</div>
<div className="flex flex-col">
<span className="text-sm font-black text-gray-900">{t.customer_name || t.source_ref_id || 'Sistem Ödemesi'}</span>
<span className="text-[10px] text-gray-400 font-bold truncate max-w-[150px] mt-1">{t.customer_phone || t.callback_url || 'doğrudan-ödeme'}</span>
</div>
</div>
</td>
<td className="px-10 py-8">
<span className="text-xs font-bold text-gray-500 uppercase">
{format(new Date(t.created_at), 'dd MMM yyyy', { locale: tr })}
</span>
</td>
<td className="px-10 py-8 text-right pr-20">
<span className="text-sm font-black text-gray-900">
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
</span>
</td>
<td className="px-10 py-8">
<div className="flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${t.status === 'succeeded' ? 'bg-emerald-50 text-emerald-600' :
t.status === 'failed' ? 'bg-red-50 text-red-600' :
'bg-orange-50 text-orange-600'
}`}>
{t.status === 'succeeded' ? 'Başarılı' :
t.status === 'failed' ? 'Hatalı' : 'Bekliyor'}
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

138
app/admin/settings/page.tsx Normal file
View File

@@ -0,0 +1,138 @@
'use client';
import React from 'react';
import {
Globe,
ShieldCheck,
Bell,
Trash2,
Smartphone,
Monitor,
} from 'lucide-react';
export default function SettingsPage() {
return (
<div className="max-w-4xl space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Ayarlar</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Platform tercihlerinizi yönetin</p>
</div>
<button className="px-8 py-4 bg-gray-900 text-white rounded-2xl font-black text-sm shadow-xl shadow-gray-200 hover:bg-gray-800 transition active:scale-95">
Değişiklikleri Kaydet
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
{/* Left Column: Sections */}
<div className="lg:col-span-2 space-y-8">
{/* General Section */}
<section className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
<div className="flex items-center gap-4 border-b border-gray-50 pb-6">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
<Globe size={24} />
</div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Genel Ayarlar</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Mağaza Adı</label>
<input type="text" defaultValue="froyd Store" 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" />
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Destek E-postası</label>
<input type="email" defaultValue="support@froyd.io" 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" />
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Para Birimi</label>
<select 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 appearance-none">
<option>Türk Lirası ()</option>
<option>US Dollar ($)</option>
<option>Euro ()</option>
</select>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Zaman Dilimi</label>
<select 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 appearance-none">
<option>Istanbul (GMT+3)</option>
<option>London (GMT+0)</option>
<option>New York (EST)</option>
</select>
</div>
</div>
</section>
{/* Security Section */}
<section className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
<div className="flex items-center gap-4 border-b border-gray-50 pb-6">
<div className="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-600">
<ShieldCheck size={24} />
</div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Güvenlik</h2>
</div>
<div className="space-y-6">
<div className="flex items-center justify-between p-6 bg-gray-50 rounded-[32px]">
<div className="space-y-1">
<p className="text-sm font-black text-gray-900">İki Faktörlü Doğrulama</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">Hesabınıza ekstra bir güvenlik katmanı ekleyin</p>
</div>
<div className="w-12 h-6 bg-blue-500 rounded-full relative cursor-pointer">
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between p-6 bg-gray-50 rounded-[32px]">
<div className="space-y-1">
<p className="text-sm font-black text-gray-900">API Erişimi</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">Harici uygulamalar için anahtar yönetimi</p>
</div>
<button className="text-blue-600 text-[10px] font-black uppercase tracking-widest hover:underline">Anahtarları Düzenle</button>
</div>
</div>
</section>
</div>
{/* Right Column: Notifications & Danger Zone */}
<div className="space-y-8">
<section className="bg-white p-8 rounded-[40px] border border-gray-100 shadow-sm space-y-6">
<div className="flex items-center gap-4 mb-2">
<div className="w-10 h-10 bg-orange-50 rounded-xl flex items-center justify-center text-orange-600">
<Bell size={20} />
</div>
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tight">Bildirimler</h2>
</div>
<div className="space-y-4">
{['Yeni Satışlar', 'Müşteri Mesajları', 'Sistem Güncellemeleri'].map((item) => (
<label key={item} className="flex items-center gap-4 cursor-pointer group">
<input type="checkbox" defaultChecked className="w-5 h-5 rounded-lg border-2 border-gray-100 text-blue-600 focus:ring-blue-500" />
<span className="text-sm font-bold text-gray-500 group-hover:text-gray-900 transition">{item}</span>
</label>
))}
</div>
</section>
<section className="bg-red-50 p-8 rounded-[40px] border border-red-100 space-y-6">
<div className="flex items-center gap-4 text-red-600">
<Trash2 size={20} />
<h2 className="text-lg font-black uppercase tracking-tight">Tehlikeli Bölge</h2>
</div>
<p className="text-xs font-bold text-red-400 leading-relaxed uppercase tracking-wider">
Mağaza verilerini kalıcı olarak silmek veya hesabı kapatmak için bu bölümü kullanın. Bu işlem geri alınamaz.
</p>
<button className="w-full py-4 bg-red-600 text-white rounded-2xl font-black text-sm hover:bg-red-700 transition shadow-lg shadow-red-100">
Mağazayı Devre Dışı Bırak
</button>
</section>
</div>
</div>
{/* Footer Meta */}
<div className="pt-12 text-center">
<p className="text-[10px] text-gray-300 font-black uppercase tracking-[0.4em]">v1.0.4 Platinum Enterprise Edition</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase';
import {
Search,
Filter,
Download,
ExternalLink,
MoreVertical
} from 'lucide-react';
import { format } from 'date-fns';
import { tr } from 'date-fns/locale';
async function getTransactions() {
const { data, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: false });
if (error) return [];
return data;
}
export default async function TransactionsPage() {
const transactions = await getTransactions();
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Search and Filters Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
<div className="flex items-center gap-4 flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="İşlem ID veya referans ile ara..."
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"
/>
</div>
<button className="flex items-center gap-2 px-6 py-3 bg-white border border-gray-100 rounded-2xl text-sm font-bold text-gray-600 hover:bg-gray-50 transition">
<Filter size={18} />
Filtreler
</button>
</div>
<button className="flex items-center justify-center gap-2 px-6 py-3 bg-gray-900 text-white rounded-2xl text-sm font-bold hover:bg-gray-800 transition shadow-lg shadow-gray-200">
<Download size={18} />
CSV Olarak İndir
</button>
</div>
{/* Full Transactions Table */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden text-sans tracking-tight">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/30 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
<th className="px-10 py-6">İşlem ID</th>
<th className="px-10 py-6">Referans / Kaynak</th>
<th className="px-10 py-6">Tarih & Saat</th>
<th className="px-10 py-6">Tutar</th>
<th className="px-10 py-6 text-center">Durum</th>
<th className="px-10 py-6 text-right">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{transactions.map((t) => (
<tr key={t.id} className="group hover:bg-gray-50/50 transition-colors">
<td className="px-10 py-8">
<span className="text-sm font-black text-gray-900 uppercase">#{t.stripe_pi_id?.slice(-12).toUpperCase() || 'MOCK'}</span>
</td>
<td className="px-10 py-8">
<div className="flex flex-col">
<span className="text-sm font-bold text-gray-900">{t.customer_name || t.source_ref_id || 'Doğrudan Ödeme'}</span>
{t.customer_phone ? (
<span className="text-[10px] text-blue-600 font-black uppercase tracking-wider mt-1">{t.customer_phone}</span>
) : (
<span className="text-[10px] text-gray-400 font-bold truncate max-w-[200px] mt-1">{t.callback_url || 'Geri dönüş yok'}</span>
)}
</div>
</td>
<td className="px-10 py-8">
<span className="text-xs font-bold text-gray-500">
{format(new Date(t.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })}
</span>
</td>
<td className="px-10 py-8 font-black text-gray-900">
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })} {t.currency.toUpperCase() === 'TRY' ? '₺' : t.currency.toUpperCase()}
</td>
<td className="px-10 py-8">
<div className="flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${t.status === 'succeeded' ? 'bg-emerald-50 text-emerald-600' :
t.status === 'failed' ? 'bg-red-50 text-red-600' :
'bg-orange-50 text-orange-600'
}`}>
{t.status === 'succeeded' ? 'Başarılı' :
t.status === 'failed' ? 'Hatalı' : 'Bekliyor'}
</span>
</div>
</td>
<td className="px-10 py-8 text-right">
<div className="flex items-center justify-end gap-2">
{t.callback_url && (
<a href={t.callback_url} target="_blank" className="p-2 text-gray-300 hover:text-blue-600 transition">
<ExternalLink size={18} />
</a>
)}
<button className="p-2 text-gray-300 hover:text-gray-900 transition">
<MoreVertical size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{transactions.length === 0 && (
<div className="p-20 text-center space-y-4">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto text-gray-300">
<Search size={32} />
</div>
<p className="text-gray-400 font-bold uppercase tracking-widest text-xs">İşlem bulunamadı</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { supabaseAdmin } from '@/lib/supabase';
export async function POST(req: NextRequest) {
try {
const { amount, currency, ref_id, callback_url, customer_name, customer_phone } = await req.json();
if (!amount || !currency) {
return NextResponse.json(
{ error: 'Tutar ve para birimi zorunludur.' },
{ status: 400 }
);
}
const useMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
let clientSecret = 'mock_secret_' + Math.random().toString(36).substring(7);
let stripeId = 'mock_pi_' + Math.random().toString(36).substring(7);
if (!useMock) {
// 1. Create PaymentIntent in Stripe
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Stripe uses subunits (e.g., cents)
currency: currency.toLowerCase(),
metadata: {
ref_id,
callback_url,
customer_name,
customer_phone,
},
});
clientSecret = paymentIntent.client_secret!;
stripeId = paymentIntent.id;
}
// 2. Log transaction in Supabase with 'pending' status
const { error: dbError } = await supabaseAdmin
.from('transactions')
.insert({
amount,
currency,
status: 'pending',
stripe_pi_id: stripeId,
source_ref_id: ref_id,
customer_name,
customer_phone,
callback_url,
});
if (dbError) {
console.error('Database log error:', dbError);
}
return NextResponse.json({
clientSecret: clientSecret,
});
} catch (err: any) {
console.error('Internal Error:', err);
return NextResponse.json(
{ error: `Internal Server Error: ${err.message}` },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
export async function POST(req: NextRequest) {
try {
const { clientSecret, status, customer_name, customer_phone } = await req.json();
if (process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS !== 'true') {
return NextResponse.json({ error: 'Mock payments are disabled' }, { status: 403 });
}
// Update transaction in Supabase
const { error } = await supabaseAdmin
.from('transactions')
.update({
status,
customer_name,
customer_phone
})
.eq('stripe_pi_id', clientSecret); // In mock mode, we use clientSecret as the ID
if (error) {
console.error('Mock update DB error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ success: true });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
export async function POST(req: NextRequest) {
try {
const { stripe_id, customer_name, customer_phone } = await req.json();
if (!stripe_id) {
return NextResponse.json({ error: 'Missing stripe_id' }, { status: 400 });
}
const { error } = await supabaseAdmin
.from('transactions')
.update({
customer_name,
customer_phone
})
.eq('stripe_pi_id', stripe_id);
if (error) {
console.error('Update transaction info error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ success: true });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { supabaseAdmin } from '@/lib/supabase';
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
} catch (err: any) {
console.error(`Webhook Error: ${err.message}`);
return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
}
const session = event.data.object as any;
// Handle the business logic based on event type
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(session);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailed(session);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
return NextResponse.json({ received: true });
}
async function handlePaymentSucceeded(paymentIntent: any) {
const { error } = await supabaseAdmin
.from('transactions')
.update({ status: 'succeeded' })
.eq('stripe_pi_id', paymentIntent.id);
if (error) {
console.error('Error updating transaction success:', error);
}
}
async function handlePaymentFailed(paymentIntent: any) {
const { error } = await supabaseAdmin
.from('transactions')
.update({ status: 'failed' })
.eq('stripe_pi_id', paymentIntent.id);
if (error) {
console.error('Error updating transaction failure:', error);
}
}

View File

@@ -0,0 +1,52 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { XCircle, ArrowLeft, RotateCcw } from 'lucide-react';
import Link from 'next/link';
import { Suspense } from 'react';
function ErrorContent() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callback_url') || '/';
return (
<div className="max-w-md mx-auto mt-20 p-8 text-center bg-white rounded-3xl shadow-xl border border-red-50">
<div className="flex justify-center mb-6">
<div className="bg-red-100 p-4 rounded-full">
<XCircle className="w-16 h-16 text-red-600" />
</div>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Ödeme Başarısız</h1>
<p className="text-gray-600 mb-8">
İşleminiz maalesef tamamlanamadı. Kart bilgilerinizi kontrol edip tekrar deneyebilirsiniz.
</p>
<div className="flex flex-col gap-3">
<Link
href="/checkout"
className="flex items-center justify-center gap-2 w-full py-4 bg-gray-900 text-white rounded-2xl font-bold hover:bg-gray-800 transition"
>
<RotateCcw size={18} />
Tekrar Dene
</Link>
<Link
href={callbackUrl}
className="flex items-center justify-center gap-2 w-full py-4 bg-white text-gray-600 border border-gray-100 rounded-2xl font-bold hover:bg-gray-50 transition"
>
<ArrowLeft size={18} />
Mağazaya Dön
</Link>
</div>
</div>
);
}
export default function CheckoutErrorPage() {
return (
<div className="min-h-screen bg-[#F8FAFC] flex items-center justify-center p-4">
<Suspense fallback={<div>Yükleniyor...</div>}>
<ErrorContent />
</Suspense>
</div>
);
}

163
app/checkout/page.tsx Normal file
View File

@@ -0,0 +1,163 @@
'use client';
import React, { useEffect, useState, Suspense } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { getStripe } from '@/lib/stripe-client';
import { useSearchParams } from 'next/navigation';
import CheckoutForm from '@/components/checkout/CheckoutForm';
import MockCheckoutForm from '@/components/checkout/MockCheckoutForm';
import { Loader2, AlertCircle, ArrowLeft, UserCircle } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link'; // Added Link import
function CheckoutContent() {
const searchParams = useSearchParams();
const amount = parseFloat(searchParams.get('amount') || '100');
const currency = searchParams.get('currency') || 'TL';
const refId = searchParams.get('ref_id') || 'SEC-99231-TX';
const callbackUrl = searchParams.get('callback_url') || '/';
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const isMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
useEffect(() => {
if (amount <= 0) {
setError('Geçersiz işlem tutarı.');
return;
}
fetch('/api/create-payment-intent', {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount, currency, ref_id: refId, callback_url: callbackUrl }),
})
.then((res) => res.json())
.then((data) => {
if (data.error) {
setError(data.error);
} else {
setClientSecret(data.clientSecret);
}
})
.catch(() => setError('Ödeme başlatılamadı. Lütfen tekrar deneyin.'));
}, [amount, currency, refId, callbackUrl]);
if (error) {
return (
<div className="flex flex-col items-center justify-center p-8 bg-red-50 rounded-2xl border border-red-100 max-w-md mx-auto mt-20">
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<h2 className="text-xl font-bold text-red-700 mb-2">Hata Oluştu</h2>
<p className="text-red-600 text-center">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-6 px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
>
Tekrar Dene
</button>
</div>
);
}
return (
<div className="min-h-screen bg-[#F8FAFC] flex flex-col">
{/* Header */}
<nav className="bg-white border-b border-gray-100 px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-white rotate-45 transform"></div>
</div>
<span className="font-bold text-gray-900 text-lg tracking-tight">froydPay</span>
</div>
<div className="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center text-orange-500">
<UserCircle size={24} />
</div>
</nav>
{/* Main Content */}
<div className="flex-1 flex flex-col lg:flex-row items-stretch max-w-7xl mx-auto w-full px-6 py-12 gap-12">
{/* Left Column: Product Info */}
<div className="flex-1 flex flex-col justify-center items-center lg:items-end space-y-8 order-2 lg:order-1">
<div className="relative group perspective-1000">
<div className="w-full max-w-[400px] aspect-square relative rounded-[40px] overflow-hidden shadow-2xl shadow-blue-200/50 transform group-hover:rotate-y-6 transition-transform duration-500 border-8 border-white">
<Image
src="/digital_nft_asset.png"
alt="Digital NFT Product"
fill
className="object-cover"
priority
/>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-8 pt-20">
<span className="text-blue-400 text-[10px] font-bold uppercase tracking-widest mb-2 block">Premium Dijital Varlık</span>
<h3 className="text-white text-2xl font-black tracking-tight uppercase">CyberCube #082</h3>
<p className="text-gray-300 text-sm mt-2 line-clamp-2">froyd ına ömür boyu erişim sağlayan özel, yüksek sadakatli 3D üretken dijital koleksiyon parçası.</p>
</div>
</div>
{/* Gloss Effect */}
<div className="absolute inset-0 rounded-[40px] bg-gradient-to-tr from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
</div>
<div className="text-center lg:text-right space-y-2 hidden lg:block">
<p className="text-gray-400 text-sm font-medium">Satıcı: <span className="text-gray-900 uppercase">Froyd Digital Media INC.</span></p>
<p className="text-gray-400 text-sm">Müşteri Desteği: <span className="text-blue-600 hover:underline cursor-pointer">help@froyd.io</span></p>
</div>
</div>
{/* Right Column: Payment Form */}
<div className="flex-1 flex flex-col justify-center items-center lg:items-start order-1 lg:order-2">
{!clientSecret ? (
<div className="flex flex-col items-center justify-center p-20 bg-white rounded-3xl border border-gray-100 shadow-sm w-full max-w-md">
<Loader2 className="w-10 h-10 text-blue-600 animate-spin mb-4" />
<p className="text-gray-500 font-medium">Ödeme ekranı hazırlanıyor...</p>
</div>
) : (
<div className="w-full">
{isMock ? (
<MockCheckoutForm amount={amount} currency={currency} callbackUrl={callbackUrl} clientSecret={clientSecret} refId={refId} />
) : (
<Elements stripe={getStripe()} options={{ clientSecret, appearance: { theme: 'stripe' } }}>
<CheckoutForm
amount={amount}
currency={currency}
callbackUrl={callbackUrl}
piId={clientSecret.split('_secret')[0]}
/>
</Elements>
)}
<div className="mt-8 flex justify-center lg:justify-start">
<Link href={callbackUrl} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
<ArrowLeft size={16} />
Mağazaya Dön
</Link>
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<footer className="py-12 border-t border-gray-100 text-center">
<p className="text-[#94A3B8] text-[10px] font-medium tracking-tight uppercase">
© 2026 froydPay Inc. Tüm hakları saklıdır.
</p>
</footer>
</div>
);
}
export default function CheckoutPage() {
return (
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<Suspense fallback={
<div className="flex justify-center items-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
}>
<CheckoutContent />
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,72 @@
'use client';
import React, { useEffect, Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { CheckCircle2, Loader2 } from 'lucide-react';
function SuccessContent() {
const searchParams = useSearchParams();
const router = useRouter();
const callbackUrl = searchParams.get('callback_url');
const paymentIntent = searchParams.get('payment_intent');
useEffect(() => {
if (callbackUrl) {
// Redirect after a short delay
const timer = setTimeout(() => {
try {
// Handle potential relative URLs by providing a base
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
const url = new URL(callbackUrl, baseUrl);
url.searchParams.append('status', 'success');
if (paymentIntent) url.searchParams.append('payment_intent', paymentIntent);
window.location.href = url.toString();
} catch (e) {
console.error('URL parse error:', e);
// Fallback to direct navigation if URL parsing fails
window.location.href = callbackUrl;
}
}, 3000);
return () => clearTimeout(timer);
}
}, [callbackUrl, paymentIntent]);
return (
<div className="max-w-md mx-auto mt-20 p-8 text-center bg-white rounded-3xl shadow-xl border border-green-50">
<div className="flex justify-center mb-6">
<div className="bg-green-100 p-4 rounded-full">
<CheckCircle2 className="w-16 h-16 text-green-600" />
</div>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Ödeme Başarılı!</h1>
<p className="text-gray-600 mb-8">
İşleminiz başarıyla tamamlandı. Birazdan geldiğiniz sayfaya yönlendirileceksiniz.
</p>
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-6 h-6 text-green-600 animate-spin" />
<p className="text-sm text-gray-400">Yönlendiriliyor...</p>
{callbackUrl && (
<a
href={callbackUrl}
className="text-blue-600 hover:underline text-sm font-medium mt-4"
>
Yönlendirme olmazsa buraya tıklayın
</a>
)}
</div>
</div>
);
}
export default function SuccessPage() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<Suspense fallback={<Loader2 className="w-8 h-8 animate-spin text-blue-600" />}>
<SuccessContent />
</Suspense>
</div>
);
}

99
app/login/page.tsx Normal file
View File

@@ -0,0 +1,99 @@
'use client';
import { useState } from 'react';
import { createClient } from '@/utils/supabase/client';
import { useRouter } from 'next/navigation';
import { Lock, Mail, Loader2, Wallet } from 'lucide-react';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const supabase = createClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
setIsLoading(false);
} else {
router.push('/admin');
router.refresh();
}
};
return (
<div className="min-h-screen bg-[#F8FAFC] flex items-center justify-center p-4">
<div className="w-full max-w-md space-y-8 bg-white p-10 rounded-[40px] border border-gray-100 shadow-xl shadow-blue-50">
<div className="text-center space-y-4">
<div className="mx-auto w-16 h-16 bg-[#2563EB] rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-100">
<Wallet size={32} />
</div>
<div>
<h1 className="text-2xl font-black text-gray-900 tracking-tight">Admin Login</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-1">Management Portal</p>
</div>
</div>
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Email Address</label>
<div className="relative">
<Mail className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@froyd.io"
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Password</label>
<div className="relative">
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 text-xs font-bold rounded-2xl border border-red-100 animate-shake">
{error}
</div>
)}
<button
disabled={isLoading}
className="w-full bg-gray-900 text-white font-black py-5 rounded-2xl transition duration-300 shadow-lg hover:bg-gray-800 disabled:opacity-70 flex items-center justify-center"
>
{isLoading ? <Loader2 className="animate-spin" /> : 'Log In to Dashboard'}
</button>
</form>
<p className="text-center text-[10px] text-gray-300 font-bold uppercase tracking-widest">
Secure encrypted session
</p>
</div>
</div>
);
}

View File

@@ -1,65 +1,93 @@
import Image from "next/image";
'use client';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { ShieldCheck, CreditCard, LayoutDashboard, Zap } from 'lucide-react';
export default function Home() {
const [randomAmount, setRandomAmount] = useState(150);
const [refId, setRefId] = useState('DEMO-123');
const [mounted, setMounted] = useState(false);
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}`);
}, []);
// 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.
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="min-h-screen bg-white">
{/* Hero Section */}
<header className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-16 text-center">
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-blue-50 text-blue-700 text-sm font-medium mb-6">
<Zap size={16} />
<span>v1.0.0 Yayında</span>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<h1 className="text-5xl md:text-7xl font-black text-gray-900 tracking-tight mb-8">
Ödemelerinizi <span className="text-blue-600 underline decoration-blue-200 underline-offset-8">froyd</span> ile Güvenceye Alın
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-10">
Stripe altyapısı ile projelerinize kolayca ödeme geçidi ekleyin. Merkezi yönetim paneli ile tüm işlemlerinizi tek bir yerden takip edin.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href={`/checkout?amount=${randomAmount}&currency=TRY&ref_id=${refId}&callback_url=http://localhost:3000`}
className="px-8 py-4 bg-gray-900 text-white rounded-2xl font-bold text-lg hover:bg-gray-800 transition shadow-xl transition-all hover:scale-105 active:scale-95"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
{mounted ? `Test Ödemesi Başlat (${randomAmount.toLocaleString('tr-TR')} ₺)` : 'Ödeme Sayfasını Test Et'}
</Link>
<Link
href="/admin"
className="px-8 py-4 bg-white text-gray-900 border-2 border-gray-100 rounded-2xl font-bold text-lg hover:border-gray-200 transition"
>
Documentation
</a>
Admin Panelini Gör
</Link>
</div>
</main>
</header>
{/* Features */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 border-t border-gray-50">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="space-y-4">
<div className="bg-blue-600 w-12 h-12 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
<ShieldCheck size={24} />
</div>
<h3 className="text-xl font-bold text-gray-900">Güvenli Altyapı</h3>
<p className="text-gray-600 leading-relaxed">
Stripe Elements kullanarak kart bilgilerini asla sunucularınızda saklamazsınız. Tam güvenlik garantisi.
</p>
</div>
<div className="space-y-4">
<div className="bg-purple-600 w-12 h-12 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-purple-200">
<CreditCard size={24} />
</div>
<h3 className="text-xl font-bold text-gray-900">Dinamik Ödeme</h3>
<p className="text-gray-600 leading-relaxed">
Herhangi bir URL parametresi ile ödeme başlatın. Projelerinize entegre etmek sadece bir dakika sürer.
</p>
</div>
<div className="space-y-4">
<div className="bg-orange-600 w-12 h-12 rounded-2xl flex items-center justify-center text-white shadow-lg shadow-orange-200">
<LayoutDashboard size={24} />
</div>
<h3 className="text-xl font-bold text-gray-900">Merkezi Takip</h3>
<p className="text-gray-600 leading-relaxed">
Tüm projelerinizden gelen ödemeleri tek bir admin panelinden, anlık grafikler ve raporlarla izleyin.
</p>
</div>
</div>
</section>
{/* Footer */}
<footer className="py-12 border-t border-gray-100 text-center">
<p className="text-gray-400 text-sm">© 2026 froyd Payment Platforms. Tüm hakları saklıdır.</p>
</footer>
</div>
);
}