Files
marmarislocal/app/[locale]/admin/blog/[id]/BlogForm.tsx
T

400 lines
17 KiB
TypeScript
Raw 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 { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createOrUpdateBlogPostAction } from '@/app/actions'
import { ArrowLeft, Save, Eye, Edit3 } from 'lucide-react'
import { Link } from '@/i18n/routing'
interface Option {
value: string
label: string
}
interface FormProps {
post: any | null
listingOptions: Option[]
}
// Simple local markdown preview renderer helper
function previewMarkdown(md: string): string {
if (!md) return ''
let html = md
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>')
html = html.replace(/^### (.*?)$/gm, '<h4 class="text-xs font-bold font-mono text-shutter uppercase tracking-wider mt-4 mb-1">$1</h4>')
html = html.replace(/^## (.*?)$/gm, '<h3 class="text-sm font-heading font-extrabold text-pine mt-5 mb-2 lowercase">$1</h3>')
html = html.replace(/^# (.*?)$/gm, '<h2 class="text-base font-heading font-extrabold text-pine mt-6 mb-3 lowercase">$1</h2>')
html = html.replace(/^\* (.*?)$/gm, '<li class="ml-4 list-disc text-xs text-ink/80">$1</li>')
html = html.replace(/^- (.*?)$/gm, '<li class="ml-4 list-disc text-xs text-ink/80">$1</li>')
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" class="text-turquoise hover:underline" target="_blank">$1</a>')
const paragraphs = html.split(/\n\n+/)
return paragraphs.map(p => {
const t = p.trim()
if (!t) return ''
if (t.startsWith('<h') || t.startsWith('<li') || t.startsWith('<ul')) return t
return `<p class="text-xs sm:text-sm text-ink/75 leading-relaxed mb-3">${t.replace(/\n/g, '<br/>')}</p>`
}).join('\n')
}
export default function BlogForm({ post, listingOptions }: FormProps) {
const router = useRouter()
const [activeTab, setActiveTab] = useState<'tr' | 'en' | 'ru'>('tr')
const [editorMode, setEditorMode] = useState<'edit' | 'preview'>('edit')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
// Markdown content states for live preview
const [contentTr, setContentTr] = useState(post?.contentTr || '')
const [contentEn, setContentEn] = useState(post?.contentEn || '')
const [contentRu, setContentRu] = useState(post?.contentRu || '')
// Selected Listings
const [selectedListings, setSelectedListings] = useState<string[]>(post?.relatedListingIds || [])
const [listingSearch, setListingSearch] = useState('')
const handleListingToggle = (id: string) => {
setSelectedListings(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
)
}
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
setError('')
setSuccess(false)
const formData = new FormData(e.currentTarget)
if (post) {
formData.append('id', post.id)
}
formData.append('relatedListingIds', selectedListings.join(','))
try {
const res = await createOrUpdateBlogPostAction(formData)
if (res.error) {
setError(res.error)
} else {
setSuccess(true)
setTimeout(() => {
router.push('/admin/blog')
router.refresh()
}, 1500)
}
} catch (err) {
setError('İşlem sırasında bir hata oluştu.')
} finally {
setLoading(false)
}
}
const filteredListings = listingOptions.filter(opt =>
opt.label.toLowerCase().includes(listingSearch.toLowerCase())
)
const activeContent = activeTab === 'tr' ? contentTr : activeTab === 'en' ? contentEn : contentRu
return (
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-bougainvillea/10 border border-bougainvillea/25 text-bougainvillea p-4 rounded-xl text-xs font-semibold">
{error}
</div>
)}
{success && (
<div className="bg-turquoise/10 border border-turquoise/25 text-turquoise p-4 rounded-xl text-xs font-bold">
Yazı başarıyla kaydedildi! Yönlendiriliyorsunuz...
</div>
)}
{/* Language tabs */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between border-b border-pine/8 gap-4 pb-1">
<div className="flex gap-2">
{(['tr', 'en', 'ru'] as const).map((lang) => {
const label = lang === 'tr' ? '🇹🇷 Türkçe' : lang === 'en' ? '🇬🇧 English' : '🇷🇺 Русский'
const isTabActive = activeTab === lang
return (
<button
key={lang}
type="button"
onClick={() => setActiveTab(lang)}
className={`py-3 px-4 font-heading font-bold text-xs border-b-2 transition lowercase ${
isTabActive
? 'border-turquoise text-turquoise'
: 'border-transparent text-shutter hover:text-pine'
}`}
>
{label}
</button>
)
})}
</div>
{/* Edit / Preview controls */}
<div className="flex gap-1 bg-stone/50 p-1 rounded-xl border border-pine/5 shrink-0 self-start">
<button
type="button"
onClick={() => setEditorMode('edit')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-mono font-bold uppercase transition ${
editorMode === 'edit' ? 'bg-pine text-stone' : 'text-pine/70 hover:text-pine'
}`}
>
<Edit3 className="w-3.5 h-3.5" />
düzenle
</button>
<button
type="button"
onClick={() => setEditorMode('preview')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-mono font-bold uppercase transition ${
editorMode === 'preview' ? 'bg-pine text-stone' : 'text-pine/70 hover:text-pine'
}`}
>
<Eye className="w-3.5 h-3.5" />
önizleme
</button>
</div>
</div>
{/* Localized inputs */}
<div className="space-y-4">
{activeTab === 'tr' && (
<div className="space-y-4">
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yazı Başlığı (TR) *</label>
<input
type="text"
name="titleTr"
required
defaultValue={post?.titleTr || ''}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
placeholder="Örn: Marmaris'te Nerede Yenir? 2026 Rehberi"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">İçerik (TR - Markdown) *</label>
{editorMode === 'edit' ? (
<textarea
name="contentTr"
required
rows={10}
value={contentTr}
onChange={(e) => setContentTr(e.target.value)}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm font-medium focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
placeholder="Başlıklar için ##, listeler için - kullanın..."
/>
) : (
<div
className="w-full bg-stone/30 border border-dashed border-pine/10 rounded-xl px-6 py-4 min-h-[220px]"
dangerouslySetInnerHTML={{ __html: previewMarkdown(contentTr) }}
/>
)}
</div>
</div>
)}
{activeTab === 'en' && (
<div className="space-y-4">
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yazı Başlığı (EN) *</label>
<input
type="text"
name="titleEn"
required
defaultValue={post?.titleEn || ''}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
placeholder="Örn: Where to Eat in Marmaris? 2026 Guide"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">İçerik (EN - Markdown) *</label>
{editorMode === 'edit' ? (
<textarea
name="contentEn"
required
rows={10}
value={contentEn}
onChange={(e) => setContentEn(e.target.value)}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm font-medium focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
placeholder="English text in markdown..."
/>
) : (
<div
className="w-full bg-stone/30 border border-dashed border-pine/10 rounded-xl px-6 py-4 min-h-[220px]"
dangerouslySetInnerHTML={{ __html: previewMarkdown(contentEn) }}
/>
)}
</div>
</div>
)}
{activeTab === 'ru' && (
<div className="space-y-4">
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yazı Başlığı (RU) *</label>
<input
type="text"
name="titleRu"
required
defaultValue={post?.titleRu || ''}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
placeholder="Örn: Где поесть в Мармарисе? Путеводитель 2026"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">İçerik (RU - Markdown) *</label>
{editorMode === 'edit' ? (
<textarea
name="contentRu"
required
rows={10}
value={contentRu}
onChange={(e) => setContentRu(e.target.value)}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm font-medium focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
placeholder="Russian text in markdown..."
/>
) : (
<div
className="w-full bg-stone/30 border border-dashed border-pine/10 rounded-xl px-6 py-4 min-h-[220px]"
dangerouslySetInnerHTML={{ __html: previewMarkdown(contentRu) }}
/>
)}
</div>
</div>
)}
</div>
<hr className="border-dashed border-pine/8" />
{/* Global settings */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Slug */}
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">URL Slug *</label>
<input
type="text"
name="slug"
required
defaultValue={post?.slug || ''}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono"
placeholder="marmariste-nerede-yenir-2026"
/>
</div>
{/* Tags */}
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Etiketler (Virgülle ayırın)</label>
<input
type="text"
name="tags"
defaultValue={post?.tags?.join(', ') || ''}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink font-mono text-xs"
placeholder="Gezilecek Yerler, Restoranlar, Rehber"
/>
</div>
{/* Cover image upload */}
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Kapak Görseli</label>
<input
type="file"
name="coverImageFile"
accept="image/*"
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink file:mr-3 file:py-1 file:px-2.5 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-pine file:text-stone hover:file:opacity-90 cursor-pointer"
/>
{post?.coverImage && (
<div className="mt-2 text-xs flex items-center gap-2">
<span className="text-shutter/65">Mevcut Kapak:</span>
<a href={post.coverImage} target="_blank" rel="noopener noreferrer" className="text-turquoise hover:underline font-mono truncate max-w-xs">{post.coverImage}</a>
<input type="hidden" name="coverImageUrl" value={post.coverImage} />
</div>
)}
</div>
{/* Publish Status */}
<div className="space-y-1.5">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yayın Durumu</label>
<select
name="isPublished"
defaultValue={post?.publishedAt ? 'true' : 'false'}
className="w-full bg-stone border border-pine/10 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-turquoise outline-none text-ink"
>
<option value="false">Taslak (Yayınlama)</option>
<option value="true">Yayında (Aktif)</option>
</select>
</div>
</div>
{/* Related Listings checkbox selection box */}
<div className="space-y-2 border-t border-dashed border-pine/8 pt-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<label className="block text-xs font-mono uppercase tracking-wider text-shutter">Yazıyla İlişkili Mekanlar (Internal Links)</label>
<input
type="text"
value={listingSearch}
onChange={(e) => setListingSearch(e.target.value)}
className="bg-stone border border-pine/10 rounded-xl px-3 py-1.5 text-xs outline-none focus:ring-1 focus:ring-turquoise w-full sm:w-64 placeholder:text-shutter/60"
placeholder="Mekan ara..."
/>
</div>
<div className="border border-pine/10 bg-stone/20 rounded-2xl max-h-52 overflow-y-auto p-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
{filteredListings.length === 0 ? (
<span className="text-xs text-shutter/60 col-span-2 py-4 text-center font-mono">Aradığınız mekan bulunamadı.</span>
) : (
filteredListings.map((opt) => {
const isChecked = selectedListings.includes(opt.value)
return (
<label
key={opt.value}
className={`flex items-center gap-2.5 p-2 rounded-xl border text-xs cursor-pointer select-none transition ${
isChecked ? 'bg-turquoise/5 border-turquoise/35 text-pine' : 'border-transparent text-ink/75 hover:bg-stone/50'
}`}
>
<input
type="checkbox"
checked={isChecked}
onChange={() => handleListingToggle(opt.value)}
className="accent-turquoise rounded w-3.5 h-3.5 shrink-0"
/>
<span className="truncate leading-none">{opt.label}</span>
</label>
)
})
)}
</div>
</div>
{/* Buttons */}
<div className="flex gap-4 pt-4 border-t border-dashed border-pine/8">
<Link
href="/admin/blog"
className="inline-flex items-center gap-1.5 px-5 py-3 border border-pine/20 rounded-xl text-xs font-bold text-pine hover:bg-stone/30 transition duration-150"
>
<ArrowLeft className="w-4 h-4" />
İptal
</Link>
<button
type="submit"
disabled={loading}
className="inline-flex items-center gap-1.5 px-5 py-3 bg-turquoise hover:bg-turquoise/90 disabled:opacity-75 text-paper rounded-xl text-xs font-bold shadow-sm transition duration-150"
>
<Save className="w-4 h-4" />
{loading ? 'Kaydediliyor...' : 'Yazıyı Kaydet'}
</button>
</div>
</form>
)
}