Files

558 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useState } from 'react'
import { Plus, Play, Edit, Trash2, CheckCircle2, XCircle, Search, ExternalLink, Video } from 'lucide-react'
import {
createTutorialAction,
updateTutorialAction,
deleteTutorialAction,
toggleTutorialStatusAction,
} from './actions'
import { extractYoutubeId } from './utils'
export interface TutorialItem {
id: string
title: string
description: string
category: string
category_label: string
duration: string
youtube_id: string
target_view: string | null
target_view_label: string | null
highlights: string[]
sort_order: number
is_active: boolean
created_at: string
}
const CATEGORY_OPTIONS = [
{ id: 'basics', label: '🚀 Hızlı Başlangıç' },
{ id: 'uyap', label: '⚖️ UYAP & Eklenti' },
{ id: 'ai_drafting', label: '🤖 AI Dilekçe & Savunma' },
{ id: 'jurisprudence', label: '🔍 18M İçtihat' },
{ id: 'cases', label: '📑 Dava & Tensip Analizi' },
{ id: 'templates', label: '📚 Şablonlar & Mevzuat' },
]
const TARGET_VIEWS = [
{ id: 'overview', label: 'Genel Bakış' },
{ id: 'cases', label: 'Dava Dosyaları' },
{ id: 'research', label: 'İçtihat Arama' },
{ id: 'drafting', label: 'Dilekçe Yazımı' },
{ id: 'templates', label: 'Şablon Kütüphanesi' },
{ id: 'clients', label: 'Müvekkiller' },
{ id: 'chat', label: 'AI Hukuk Asistanı' },
]
export function TutorialsClient({ initialTutorials }: { initialTutorials: TutorialItem[] }) {
const [tutorials, setTutorials] = useState<TutorialItem[]>(initialTutorials)
const [search, setSearch] = useState('')
const [categoryFilter, setCategoryFilter] = useState('all')
const [previewVideo, setPreviewVideo] = useState<TutorialItem | null>(null)
// Form modal state
const [isFormOpen, setIsFormOpen] = useState(false)
const [editingItem, setEditingItem] = useState<TutorialItem | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
// Form Fields
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [category, setCategory] = useState('basics')
const [duration, setDuration] = useState('03:45')
const [youtubeInput, setYoutubeInput] = useState('')
const [targetView, setTargetView] = useState('')
const [targetViewLabel, setTargetViewLabel] = useState('')
const [highlightsText, setHighlightsText] = useState('')
const [sortOrder, setSortOrder] = useState('0')
const [isActive, setIsActive] = useState(true)
const openNewForm = () => {
setEditingItem(null)
setTitle('')
setDescription('')
setCategory('basics')
setDuration('03:00')
setYoutubeInput('')
setTargetView('overview')
setTargetViewLabel('Genel Bakışa Git')
setHighlightsText('')
setSortOrder((tutorials.length + 1).toString())
setIsActive(true)
setErrorMessage(null)
setIsFormOpen(true)
}
const openEditForm = (item: TutorialItem) => {
setEditingItem(item)
setTitle(item.title)
setDescription(item.description)
setCategory(item.category)
setDuration(item.duration)
setYoutubeInput(`https://www.youtube.com/watch?v=/${item.youtube_id}`)
setTargetView(item.target_view || '')
setTargetViewLabel(item.target_view_label || '')
setHighlightsText((item.highlights || []).join('\n'))
setSortOrder(item.sort_order.toString())
setIsActive(item.is_active)
setErrorMessage(null)
setIsFormOpen(true)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSaving(true)
setErrorMessage(null)
try {
const parsedCat = CATEGORY_OPTIONS.find((c) => c.id === category)
const catLabel = parsedCat ? parsedCat.label : '🚀 Hızlı Başlangıç'
const formData = new FormData()
if (editingItem) formData.append('id', editingItem.id)
formData.append('title', title)
formData.append('description', description)
formData.append('category', category)
formData.append('category_label', catLabel)
formData.append('duration', duration)
formData.append('youtube_id', youtubeInput)
formData.append('target_view', targetView)
formData.append('target_view_label', targetViewLabel)
formData.append('highlights', highlightsText)
formData.append('sort_order', sortOrder)
if (isActive) formData.append('is_active', 'true')
if (editingItem) {
await updateTutorialAction(formData)
const updatedYt = extractYoutubeId(youtubeInput)
setTutorials((prev) =>
prev.map((t) =>
t.id === editingItem.id
? {
...t,
title,
description,
category,
category_label: catLabel,
duration,
youtube_id: updatedYt,
target_view: targetView || null,
target_view_label: targetViewLabel || null,
highlights: highlightsText.split('\n').filter(Boolean),
sort_order: parseInt(sortOrder, 10) || 0,
is_active: isActive,
}
: t
)
)
} else {
await createTutorialAction(formData)
// Refresh full page or optimistic append
window.location.reload()
}
setIsFormOpen(false)
} catch (err: any) {
setErrorMessage(err?.message || 'Bir hata oluştu')
} finally {
setIsSaving(false)
}
}
const handleDelete = async (id: string) => {
if (!window.confirm('Bu eğitim videosunu silmek istediğinize emin misiniz?')) return
try {
await deleteTutorialAction(id)
setTutorials((prev) => prev.filter((t) => t.id !== id))
} catch (err: any) {
alert(`Silinemedi: ${err?.message}`)
}
}
const handleToggleActive = async (item: TutorialItem) => {
const nextStatus = !item.is_active
try {
await toggleTutorialStatusAction(item.id, nextStatus)
setTutorials((prev) =>
prev.map((t) => (t.id === item.id ? { ...t, is_active: nextStatus } : t))
)
} catch (err: any) {
alert(`Güncellenemedi: ${err?.message}`)
}
}
const filtered = tutorials.filter((t) => {
const matchesCat = categoryFilter === 'all' || t.category === categoryFilter
const matchesSearch =
search.trim() === '' ||
t.title.toLowerCase().includes(search.toLowerCase()) ||
t.description.toLowerCase().includes(search.toLowerCase())
return matchesCat && matchesSearch
})
return (
<div className="space-y-6">
{/* Header Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white flex items-center gap-2.5">
<Video className="w-7 h-7 text-indigo-600 dark:text-indigo-400" />
Eğitim Videoları & Akademi
</h2>
<p className="text-gray-500 dark:text-gray-400 mt-1">
AyrisLegal masaüstü uygulamasında görünen YouTube kullanım ve eğitim videolarını yönetin.
</p>
</div>
<button
onClick={openNewForm}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2.5 shadow-sm transition-colors cursor-pointer shrink-0"
>
<Plus className="w-4 h-4" />
+ Yeni Video Ekle
</button>
</div>
{/* Filter & Search Bar */}
<div className="flex flex-col sm:flex-row items-center gap-3">
<div className="relative flex-1 w-full">
<Search className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Video başlığı veya açıklamasında ara..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-sm bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-lg outline-none focus:border-indigo-500 text-gray-900 dark:text-white"
/>
</div>
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="w-full sm:w-56 px-3 py-2 text-sm bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-lg outline-none text-gray-900 dark:text-white"
>
<option value="all">Tüm Kategoriler ({tutorials.length})</option>
{CATEGORY_OPTIONS.map((c) => (
<option key={c.id} value={c.id}>
{c.label}
</option>
))}
</select>
</div>
{/* Video Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{filtered.map((item) => (
<div
key={item.id}
className="rounded-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden flex flex-col justify-between transition-all hover:shadow-md"
>
<div>
{/* Thumbnail / Embed Preview */}
<div className="relative aspect-video w-full bg-gray-900 group flex items-center justify-center overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://img.youtube.com/vi/${item.youtube_id}/mqdefault.jpg`}
alt={item.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
/>
<button
onClick={() => setPreviewVideo(item)}
className="absolute inset-0 flex items-center justify-center bg-black/40 hover:bg-black/60 transition-colors group-hover:scale-110 cursor-pointer"
title="Önizlemeyi Başlat"
>
<div className="w-12 h-12 rounded-full bg-red-600 flex items-center justify-center text-white shadow-lg">
<Play className="w-5 h-5 fill-current ml-0.5" />
</div>
</button>
<div className="absolute bottom-2 right-2 px-2 py-0.5 rounded text-[11px] font-mono bg-black/80 text-white font-bold">
{item.duration}
</div>
<div className="absolute top-2 left-2 px-2.5 py-0.5 rounded-md text-xs font-semibold bg-gray-900/90 text-indigo-300 border border-indigo-500/30">
{item.category_label}
</div>
</div>
{/* Body */}
<div className="p-4">
<div className="flex items-start justify-between gap-2 mb-1.5">
<h3 className="text-sm font-bold text-gray-900 dark:text-white line-clamp-1">
{item.title}
</h3>
<span className="text-xs font-mono text-gray-400 shrink-0">#{item.sort_order}</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mb-3">
{item.description}
</p>
{item.highlights && item.highlights.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3">
{item.highlights.slice(0, 2).map((h, i) => (
<span
key={i}
className="text-[10.5px] px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-900 text-gray-600 dark:text-gray-400"
>
{h}
</span>
))}
{item.highlights.length > 2 && (
<span className="text-[10.5px] px-1.5 py-0.5 text-gray-400">
+{item.highlights.length - 2}
</span>
)}
</div>
)}
</div>
</div>
{/* Card Footer Actions */}
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-900/60 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between">
<button
onClick={() => handleToggleActive(item)}
className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-md transition-colors ${
item.is_active
? 'bg-green-100 dark:bg-green-950/50 text-green-700 dark:text-green-400 hover:bg-green-200'
: 'bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-300'
}`}
>
{item.is_active ? <CheckCircle2 className="w-3.5 h-3.5" /> : <XCircle className="w-3.5 h-3.5" />}
{item.is_active ? 'Yayında' : 'Pasif'}
</button>
<div className="flex items-center gap-2">
<button
onClick={() => openEditForm(item)}
className="p-1.5 text-gray-500 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
title="Düzenle"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(item.id)}
className="p-1.5 text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
{filtered.length === 0 && (
<div className="p-12 text-center bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-xl text-gray-500">
Aramanıza uygun video bulunamadı.
</div>
)}
{/* YOUTUBE LIVE PREVIEW MODAL */}
{previewVideo && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="relative w-full max-w-3xl bg-gray-950 border border-gray-800 rounded-2xl overflow-hidden shadow-2xl">
<div className="flex items-center justify-between px-5 py-3.5 border-b border-gray-800 bg-gray-900/70">
<h3 className="text-sm font-bold text-white truncate">{previewVideo.title}</h3>
<button
onClick={() => setPreviewVideo(null)}
className="text-gray-400 hover:text-white text-lg"
>
</button>
</div>
<div className="aspect-video w-full">
<iframe
src={`https://www.youtube.com/embed/${previewVideo.youtube_id}?autoplay=1`}
title={previewVideo.title}
className="w-full h-full border-0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
</div>
)}
{/* CREATE / EDIT VIDEO DRAWER MODAL */}
{isFormOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm overflow-y-auto">
<div className="relative w-full max-w-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-2xl p-6 my-8">
<div className="flex items-center justify-between pb-4 border-b border-gray-100 dark:border-gray-800 mb-5">
<h3 className="text-lg font-bold text-gray-900 dark:text-white">
{editingItem ? 'Eğitim Videosunu Düzenle' : 'Yeni Eğitim Videosu Ekle'}
</h3>
<button
onClick={() => setIsFormOpen(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-white"
>
</button>
</div>
{errorMessage && (
<div className="p-3 mb-4 rounded-lg bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900 text-xs text-red-700 dark:text-red-400">
{errorMessage}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Video Başlığı *
</label>
<input
type="text"
required
placeholder="Örn: 18 Milyon İçtihat Arama Rehberi"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none focus:border-indigo-500 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Açıklama
</label>
<textarea
rows={2}
placeholder="Videonun içeriği ve kullanıcıya faydası..."
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none focus:border-indigo-500 text-gray-900 dark:text-white"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Kategori
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none text-gray-900 dark:text-white"
>
{CATEGORY_OPTIONS.map((c) => (
<option key={c.id} value={c.id}>
{c.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Süre (Örn: 04:30)
</label>
<input
type="text"
value={duration}
onChange={(e) => setDuration(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none text-gray-900 dark:text-white font-mono"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
YouTube Video Linki veya Video ID *
</label>
<input
type="text"
required
placeholder="https://www.youtube.com/watch?v=... veya dQw4w9WgXcQ"
value={youtubeInput}
onChange={(e) => setYoutubeInput(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none focus:border-indigo-500 text-gray-900 dark:text-white font-mono"
/>
<p className="text-[11px] text-gray-400 mt-1">
Normal YouTube linki, Shorts veya direct ID girebilirsiniz. Otomatik ayrıştırılır.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Yönlendirilecek Sayfa (Opsiyonel)
</label>
<select
value={targetView}
onChange={(e) => {
setTargetView(e.target.value)
const found = TARGET_VIEWS.find((v) => v.id === e.target.value)
if (found) setTargetViewLabel(`${found.label} Sayfasına Git`)
}}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none text-gray-900 dark:text-white"
>
<option value="">Seçiniz</option>
{TARGET_VIEWS.map((v) => (
<option key={v.id} value={v.id}>
{v.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Sıralama Önceliği
</label>
<input
type="number"
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none text-gray-900 dark:text-white font-mono"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1">
Öğrenilecek Başlıklar (Her satıra bir madde)
</label>
<textarea
rows={3}
placeholder="Panel kullanımı&#10;Emsal arama filtreleri&#10;Dilekçe taslağı çıkarma"
value={highlightsText}
onChange={(e) => setHighlightsText(e.target.value)}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg outline-none focus:border-indigo-500 text-gray-900 dark:text-white"
/>
</div>
<div className="flex items-center gap-2 pt-2">
<input
type="checkbox"
id="isActiveToggle"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-gray-300"
/>
<label htmlFor="isActiveToggle" className="text-xs font-semibold text-gray-700 dark:text-gray-300">
Bu videoyu uygulamada yayına al (Aktif)
</label>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-gray-100 dark:border-gray-800">
<button
type="button"
onClick={() => setIsFormOpen(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isSaving}
className="px-5 py-2 text-sm font-semibold text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors shadow-sm disabled:opacity-50"
>
{isSaving ? 'Kaydediliyor...' : editingItem ? 'Güncelle' : 'Kaydet ve Yayınla'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}