- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency. - Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation. - Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted. - Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight. - Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
117 lines
5.8 KiB
TypeScript
117 lines
5.8 KiB
TypeScript
import { Controller, useFieldArray } from 'react-hook-form';
|
|
import type { Control, UseFormRegister } from 'react-hook-form';
|
|
import type { BlogFormData } from '../lib/blogForm';
|
|
import PersianDatePicker from './ui/PersianDatePicker';
|
|
import { toGregorianDate, toTehranClockTime, tehranWallClockToUnix } from '../lib/utils';
|
|
|
|
interface Props {
|
|
control: Control<BlogFormData>;
|
|
register: UseFormRegister<BlogFormData>;
|
|
}
|
|
|
|
/** SEO + FAQ + scheduling fields, shared by admin and representative blog forms. */
|
|
export default function BlogSeoFields({ control, register }: Props) {
|
|
const { fields, append, remove } = useFieldArray({ control, name: 'faq' });
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<h3 className="section-title">سئو و زمانبندی</h3>
|
|
|
|
<div>
|
|
<label className="cp-label">عنوان متا (Meta Title)</label>
|
|
<input {...register('meta_title')} className="cp-input h-11" placeholder="عنوان برای موتور جستجو" />
|
|
</div>
|
|
|
|
<div>
|
|
<label className="cp-label">توضیح متا (Meta Description)</label>
|
|
<textarea {...register('meta_description')} rows={2} className="cp-textarea resize-none"
|
|
placeholder="حداکثر ۱۵۵ کاراکتر" />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="cp-label">کلیدواژهٔ اصلی</label>
|
|
<input {...register('primary_keyword')} className="cp-input h-11" />
|
|
</div>
|
|
<div>
|
|
<label className="cp-label">کلیدواژههای فرعی (با ویرگول)</label>
|
|
<input {...register('secondary_keywords')} className="cp-input h-11" placeholder="کلمه۱، کلمه۲" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="cp-label">لینکهای داخلی (URL با ویرگول)</label>
|
|
<input {...register('internal_links')} dir="ltr" className="cp-input h-11" placeholder="/a, /b" />
|
|
</div>
|
|
<div>
|
|
<label className="cp-label">لینکهای خارجی (URL با ویرگول)</label>
|
|
<input {...register('external_links')} dir="ltr" className="cp-input h-11" placeholder="https://..." />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="cp-label">زمان مطالعه (دقیقه)</label>
|
|
<input type="number" {...register('reading_time', { setValueAs: (v) => (v === '' || v == null ? null : Number(v)) })}
|
|
className="cp-input h-11" min={1} />
|
|
</div>
|
|
<div>
|
|
<label className="cp-label">زمانبندی انتشار</label>
|
|
<Controller
|
|
control={control}
|
|
name="scheduled_at"
|
|
render={({ field }) => <ScheduleField value={field.value ?? null} onChange={field.onChange} />}
|
|
/>
|
|
<p className="text-[12px] text-[var(--text-3)] mt-1">
|
|
خالی = انتشار دستی. مقالهٔ نیازمند بازبینی فقط پس از تأیید پزشک منتشر میشود.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="!mb-0">سوالات متداول (FAQ)</label>
|
|
<button type="button" className="mini-btn" onClick={() => append({ q: '', a: '' })}>
|
|
+ افزودن سوال
|
|
</button>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{fields.map((f, i) => (
|
|
<div key={f.id} className="card card-pad" style={{ padding: 12 }}>
|
|
<input {...register(`faq.${i}.q` as const)} className="cp-input h-10 mb-2" placeholder="سوال" />
|
|
<textarea {...register(`faq.${i}.a` as const)} rows={2} className="cp-textarea resize-none" placeholder="پاسخ" />
|
|
<button type="button" className="mini-btn danger mt-2" onClick={() => remove(i)}>حذف</button>
|
|
</div>
|
|
))}
|
|
{fields.length === 0 && <p className="muted">سوالی افزوده نشده است.</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Gregorian date + time → unix seconds, and back — همیشه به وقت ایران. */
|
|
function ScheduleField({ value, onChange }: { value: number | null; onChange: (v: number | null) => void }) {
|
|
const d = value ? new Date(value * 1000) : null;
|
|
const dateStr = d ? toGregorianDate(d) : '';
|
|
const timeStr = d ? toTehranClockTime(d) : '00:00';
|
|
|
|
const emit = (nextDate: string, nextTime: string) => {
|
|
if (!nextDate) return onChange(null);
|
|
const ts = tehranWallClockToUnix(nextDate, nextTime || '00:00');
|
|
onChange(Number.isNaN(ts) ? null : ts);
|
|
};
|
|
|
|
return (
|
|
<div className="flex gap-2 items-center">
|
|
<PersianDatePicker value={dateStr} onChange={(v) => emit(v, timeStr)} />
|
|
<input type="time" className="cp-input h-10" style={{ width: 110 }} value={timeStr}
|
|
onChange={(e) => emit(dateStr, e.target.value)} />
|
|
{value && (
|
|
<button type="button" className="mini-btn" onClick={() => onChange(null)}>پاک کردن</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|