feat: replace team members with cards in CMS, add date validation fields, and remove contact info section from user profile
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
|||||||
AboutFeaturesContent,
|
AboutFeaturesContent,
|
||||||
AboutFeatureItem,
|
AboutFeatureItem,
|
||||||
AboutTeamContent,
|
AboutTeamContent,
|
||||||
AboutTeamMember,
|
AboutTeamCard,
|
||||||
AboutCtaContent,
|
AboutCtaContent,
|
||||||
FaqContent,
|
FaqContent,
|
||||||
FaqCategoryItem,
|
FaqCategoryItem,
|
||||||
@@ -97,7 +97,7 @@ function defaultContentFor(pageSlug: string, sectionKey: string): unknown {
|
|||||||
case 'hero': return { badge: '', headline: '', description: '', ctaButtonText: '', bannerImageUrl: '', bannerOverlayText: '' } as AboutHeroContent;
|
case 'hero': return { badge: '', headline: '', description: '', ctaButtonText: '', bannerImageUrl: '', bannerOverlayText: '' } as AboutHeroContent;
|
||||||
case 'stats': return { stats: [] } as AboutStatsContent;
|
case 'stats': return { stats: [] } as AboutStatsContent;
|
||||||
case 'features': return { badge: '', features: [] } as AboutFeaturesContent;
|
case 'features': return { badge: '', features: [] } as AboutFeaturesContent;
|
||||||
case 'team': return { title: '', subtitle: '', members: [] } as AboutTeamContent;
|
case 'team': return { title: '', subtitle: '', intro: '', cards: [] } as AboutTeamContent;
|
||||||
case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent;
|
case 'cta': return { title: '', description: '', buttonText: '' } as AboutCtaContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1132,40 +1132,48 @@ export default function CmsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAboutTeamForm() {
|
function renderAboutTeamForm() {
|
||||||
const data = editContent as AboutTeamContent;
|
const dataRaw = editContent as AboutTeamContent & { members?: unknown };
|
||||||
|
const data: AboutTeamContent = {
|
||||||
|
title: dataRaw.title ?? '',
|
||||||
|
subtitle: dataRaw.subtitle ?? '',
|
||||||
|
intro: dataRaw.intro ?? '',
|
||||||
|
cards: Array.isArray(dataRaw.cards) ? dataRaw.cards : [],
|
||||||
|
};
|
||||||
const update = (patch: Partial<AboutTeamContent>) => setEditContent({ ...data, ...patch });
|
const update = (patch: Partial<AboutTeamContent>) => setEditContent({ ...data, ...patch });
|
||||||
function updateMember(index: number, patch: Partial<AboutTeamMember>) {
|
function updateCard(index: number, patch: Partial<AboutTeamCard>) {
|
||||||
const members = [...data.members];
|
const cards = [...data.cards];
|
||||||
members[index] = { ...members[index], ...patch };
|
cards[index] = { ...cards[index], ...patch };
|
||||||
update({ members });
|
update({ cards });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[#00293d] mb-1">Section Title</label>
|
<label className="block text-sm font-medium text-[#00293d] mb-1">Section Title</label>
|
||||||
<input type="text" className={inputCls} value={data.title} onChange={(e) => update({ title: e.target.value })} placeholder="e.g. Meet the minds behind the platform." />
|
<input type="text" className={inputCls} value={data.title} onChange={(e) => update({ title: e.target.value })} placeholder="e.g. For Real Estate Professionals." />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[#00293d] mb-1">Section Subtitle</label>
|
<label className="block text-sm font-medium text-[#00293d] mb-1">Section Subtitle</label>
|
||||||
<textarea rows={2} className={textareaCls} value={data.subtitle} onChange={(e) => update({ subtitle: e.target.value })} placeholder="Section description" />
|
<textarea rows={2} className={textareaCls} value={data.subtitle} onChange={(e) => update({ subtitle: e.target.value })} placeholder="A place where your skills, not your wallet, stands out." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[#00293d] mb-1">Intro Paragraph</label>
|
||||||
|
<textarea rows={4} className={textareaCls} value={data.intro} onChange={(e) => update({ intro: e.target.value })} placeholder="RE-Quest is a unique platform..." />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<label className="block text-sm font-medium text-[#00293d]">Team Members ({data.members.length})</label>
|
<label className="block text-sm font-medium text-[#00293d]">Cards ({data.cards.length})</label>
|
||||||
<button type="button" onClick={() => update({ members: [...data.members, { name: '', role: '', imageUrl: '' }] })} className="px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors">+ Add Member</button>
|
<button type="button" onClick={() => update({ cards: [...data.cards, { title: '', description: '', iconPath: '' }] })} className="px-3 py-1 text-xs bg-[#f5a623] hover:bg-[#e09620] text-white rounded-lg transition-colors">+ Add Card</button>
|
||||||
</div>
|
</div>
|
||||||
{data.members.length === 0 && <p className="text-sm text-[#666666] italic">No team members added yet.</p>}
|
{data.cards.length === 0 && <p className="text-sm text-[#666666] italic">No cards added yet.</p>}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.members.map((member, idx) => (
|
{data.cards.map((card, idx) => (
|
||||||
<div key={idx} className="p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2">
|
<div key={idx} className="p-3 border border-[#e5e7eb] rounded-lg bg-[#f9fafb] space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-semibold text-[#666666]">Member {idx + 1}</span>
|
<span className="text-xs font-semibold text-[#666666]">Card {idx + 1}</span>
|
||||||
<button type="button" onClick={() => update({ members: data.members.filter((_, i) => i !== idx) })} className="text-red-500 hover:text-red-700 text-xs font-medium">Remove</button>
|
<button type="button" onClick={() => update({ cards: data.cards.filter((_, i) => i !== idx) })} className="text-red-500 hover:text-red-700 text-xs font-medium">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{renderImageUploadField('Icon', card.iconPath, (url) => updateCard(idx, { iconPath: url }))}
|
||||||
<input type="text" className={inputCls} value={member.name} onChange={(e) => updateMember(idx, { name: e.target.value })} placeholder="Name" />
|
<input type="text" className={inputCls} value={card.title} onChange={(e) => updateCard(idx, { title: e.target.value })} placeholder="Card title" />
|
||||||
<input type="text" className={inputCls} value={member.role} onChange={(e) => updateMember(idx, { role: e.target.value })} placeholder="Role" />
|
<textarea rows={3} className={textareaCls} value={card.description} onChange={(e) => updateCard(idx, { description: e.target.value })} placeholder="Card description" />
|
||||||
</div>
|
|
||||||
{renderImageUploadField(`Photo`, member.imageUrl, (url) => updateMember(idx, { imageUrl: url }))}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -974,6 +974,28 @@ export default function SectionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{fieldForm.fieldType === 'DATE' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-[#666666] mb-1">Min Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={fieldForm.validation?.minDate ?? ''}
|
||||||
|
onChange={(e) => updateValidation('minDate', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-[#666666] mb-1">Max Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={fieldForm.validation?.maxDate ?? ''}
|
||||||
|
onChange={(e) => updateValidation('maxDate', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-xs text-[#666666] mb-1">Pattern (regex)</label>
|
<label className="block text-xs text-[#666666] mb-1">Pattern (regex)</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -464,41 +464,6 @@ export default function UserDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Information */}
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb] mb-6">
|
|
||||||
<div className="px-6 py-4 border-b border-[#e5e7eb]">
|
|
||||||
<h2 className="text-lg font-semibold text-[#00293d] font-fractul">Contact Information</h2>
|
|
||||||
</div>
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-[#666666]">Phone</p>
|
|
||||||
<p className="text-sm font-medium text-[#00293d]">
|
|
||||||
{user.profile?.phone || 'Not provided'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-[#666666]">City</p>
|
|
||||||
<p className="text-sm font-medium text-[#00293d]">
|
|
||||||
{user.profile?.city || 'Not provided'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-[#666666]">State</p>
|
|
||||||
<p className="text-sm font-medium text-[#00293d]">
|
|
||||||
{user.profile?.state || 'Not provided'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-[#666666]">Country</p>
|
|
||||||
<p className="text-sm font-medium text-[#00293d]">
|
|
||||||
{user.profile?.country || 'Not provided'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dynamic Profile Data (only for AGENT role) */}
|
{/* Dynamic Profile Data (only for AGENT role) */}
|
||||||
{user.role === 'AGENT' && (
|
{user.role === 'AGENT' && (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb] mb-6">
|
<div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb] mb-6">
|
||||||
|
|||||||
@@ -98,16 +98,17 @@ export interface AboutFeaturesContent {
|
|||||||
features: AboutFeatureItem[];
|
features: AboutFeatureItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AboutTeamMember {
|
export interface AboutTeamCard {
|
||||||
name: string;
|
title: string;
|
||||||
role: string;
|
description: string;
|
||||||
imageUrl: string;
|
iconPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AboutTeamContent {
|
export interface AboutTeamContent {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
members: AboutTeamMember[];
|
intro: string;
|
||||||
|
cards: AboutTeamCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AboutCtaContent {
|
export interface AboutCtaContent {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export type {
|
|||||||
AboutStatsContent,
|
AboutStatsContent,
|
||||||
AboutFeatureItem,
|
AboutFeatureItem,
|
||||||
AboutFeaturesContent,
|
AboutFeaturesContent,
|
||||||
AboutTeamMember,
|
AboutTeamCard,
|
||||||
AboutTeamContent,
|
AboutTeamContent,
|
||||||
AboutCtaContent,
|
AboutCtaContent,
|
||||||
FaqCategoryItem,
|
FaqCategoryItem,
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export interface FieldValidation {
|
|||||||
minLength?: number;
|
minLength?: number;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
pattern?: string;
|
pattern?: string;
|
||||||
|
minDate?: string; // ISO date (YYYY-MM-DD) for DATE fields
|
||||||
|
maxDate?: string; // ISO date (YYYY-MM-DD) for DATE fields
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldUiConfig {
|
export interface FieldUiConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user