first commit
This commit is contained in:
177
app/admin/analytics/page.tsx
Normal file
177
app/admin/analytics/page.tsx
Normal 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ı Aç
|
||||
</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>
|
||||
);
|
||||
}
|
||||
179
app/admin/customers/page.tsx
Normal file
179
app/admin/customers/page.tsx
Normal 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
137
app/admin/layout.tsx
Normal 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
327
app/admin/page.tsx
Normal 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
138
app/admin/settings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
128
app/admin/transactions/page.tsx
Normal file
128
app/admin/transactions/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
app/api/create-payment-intent/route.ts
Normal file
64
app/api/create-payment-intent/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
31
app/api/mock-complete-payment/route.ts
Normal file
31
app/api/mock-complete-payment/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
29
app/api/update-transaction-info/route.ts
Normal file
29
app/api/update-transaction-info/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
57
app/api/webhooks/stripe/route.ts
Normal file
57
app/api/webhooks/stripe/route.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
52
app/checkout/error/page.tsx
Normal file
52
app/checkout/error/page.tsx
Normal 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
163
app/checkout/page.tsx
Normal 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 ağı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>
|
||||
);
|
||||
}
|
||||
72
app/checkout/success/page.tsx
Normal file
72
app/checkout/success/page.tsx
Normal 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
99
app/login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
138
app/page.tsx
138
app/page.tsx
@@ -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}¤cy=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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user