feat(tutorials): add video training academy management with YouTube parser and live preview
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { deleteUserAction } from '../actions'
|
||||
|
||||
export default function DeleteUserButton({ id, email }: { id: string; email: string }) {
|
||||
const [confirmText, setConfirmText] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
const canDelete = confirmText.trim().toLowerCase() === email.toLowerCase()
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded-md border border-red-300 dark:border-red-900 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 text-sm font-medium px-4 py-2 transition-colors"
|
||||
>
|
||||
Kullanıcıyı Sil
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={deleteUserAction} className="space-y-3">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Bu işlem geri alınamaz — kullanıcının Supabase Auth hesabı ve profili silinir. Bu kullanıcıya bağlı lisanslar askıda kalabilir, gerekirse Lisanslar sayfasından bağlantısını kaldırın.
|
||||
Onaylamak için e-postasını yazın: <span className="font-mono text-gray-900 dark:text-white">{email}</span>
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
placeholder={email}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canDelete}
|
||||
className="rounded-md bg-red-600 hover:bg-red-700 disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm font-medium px-4 py-2 transition-colors"
|
||||
>
|
||||
Kalıcı Olarak Sil
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setOpen(false); setConfirmText('') }}
|
||||
className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white"
|
||||
>
|
||||
Vazgeç
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { supabaseAdmin, listAuthUsers } from '@/lib/supabaseAdmin'
|
||||
import { updateUserAction } from '../actions'
|
||||
import DeleteUserButton from './DeleteUserButton'
|
||||
|
||||
interface ProfileRow {
|
||||
id: string
|
||||
full_name: string | null
|
||||
bar_association_no: string | null
|
||||
bar_name: string | null
|
||||
phone: string | null
|
||||
license_status: string | null
|
||||
license_expires_at: string | null
|
||||
license_started_at: string | null
|
||||
trial_start_date: string | null
|
||||
trial_end_date: string | null
|
||||
org_id: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface LicenseRow {
|
||||
id: string
|
||||
plan: string
|
||||
status: string
|
||||
max_devices: number
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
function toDateInputValue(iso: string | null) {
|
||||
if (!iso) return ''
|
||||
return new Date(iso).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null) {
|
||||
if (!iso) return '—'
|
||||
return new Date(iso).toLocaleDateString('tr-TR', { day: 'numeric', month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
export default async function UserEditPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
searchParams: Promise<{ error?: string; success?: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const { error, success } = await searchParams
|
||||
|
||||
const [{ data: profile }, authUsers, { data: licenses }] = await Promise.all([
|
||||
supabaseAdmin
|
||||
.from('profiles')
|
||||
.select('id, full_name, bar_association_no, bar_name, phone, license_status, license_expires_at, license_started_at, trial_start_date, trial_end_date, org_id, created_at')
|
||||
.eq('id', id)
|
||||
.maybeSingle(),
|
||||
listAuthUsers(),
|
||||
supabaseAdmin.from('licenses').select('id, plan, status, max_devices, expires_at').eq('customer_id', id),
|
||||
])
|
||||
|
||||
if (!profile) notFound()
|
||||
|
||||
const p = profile as ProfileRow
|
||||
const email = authUsers.get(p.id)?.email || ''
|
||||
const linkedLicenses = (licenses as LicenseRow[] | null) || []
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<div>
|
||||
<Link href="/admin/users" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white">← Kullanıcılar</Link>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white mt-2">Kullanıcı Düzenle</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm">{email || p.id}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900 px-4 py-3 text-sm text-red-700 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="rounded-md bg-green-50 dark:bg-green-950/40 border border-green-200 dark:border-green-900 px-4 py-3 text-sm text-green-700 dark:text-green-400">
|
||||
Değişiklikler kaydedildi.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form action={updateUserAction} className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm p-6 space-y-5">
|
||||
<input type="hidden" name="id" value={p.id} />
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Ad Soyad</label>
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
defaultValue={p.full_name || ''}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baro</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bar_name"
|
||||
defaultValue={p.bar_name || ''}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Baro Sicil No</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bar_association_no"
|
||||
defaultValue={p.bar_association_no || ''}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Telefon (WhatsApp)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
placeholder="+905XXXXXXXXX"
|
||||
defaultValue={p.phone || ''}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-400">Ülke koduyla (+90) girin — WhatsApp AI hattının bu numarayı hesapla eşleştirmesi için kullanılır.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Lisans Durumu</label>
|
||||
<select
|
||||
name="license_status"
|
||||
defaultValue={p.license_status || 'active'}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="trial">trial</option>
|
||||
<option value="active">active</option>
|
||||
<option value="expired">expired</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Lisans Bitiş</label>
|
||||
<input
|
||||
type="date"
|
||||
name="license_expires_at"
|
||||
defaultValue={toDateInputValue(p.license_expires_at)}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Deneme Bitiş Tarihi</label>
|
||||
<input
|
||||
type="date"
|
||||
name="trial_end_date"
|
||||
defaultValue={toDateInputValue(p.trial_end_date)}
|
||||
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-4 py-2 transition-colors"
|
||||
>
|
||||
Kaydet
|
||||
</button>
|
||||
<Link href="/admin/users" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white">
|
||||
Vazgeç
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm p-6">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Bağlı Lisanslar</h3>
|
||||
{linkedLicenses.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Bu kullanıcıya bağlı lisans yok.</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{linkedLicenses.map((l) => (
|
||||
<li key={l.id} className="py-2.5 flex items-center justify-between text-sm">
|
||||
<span className="text-gray-700 dark:text-gray-300">{l.plan} · {l.status} · {l.max_devices} cihaz · bitiş {formatDate(l.expires_at)}</span>
|
||||
<Link href={`/admin/licenses/${l.id}`} className="text-indigo-600 dark:text-indigo-400 hover:underline">Düzenle</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-white dark:bg-gray-950 border border-red-200 dark:border-red-900 shadow-sm p-6">
|
||||
<h3 className="text-sm font-semibold text-red-700 dark:text-red-400 uppercase tracking-wide mb-3">Tehlikeli Bölge</h3>
|
||||
<DeleteUserButton id={p.id} email={email || p.id} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user