Filling the long form was blind until publish. LabProfilePreview renders a
faithful profile card from the current draft — title, bio, image, location,
skills, structured rate, languages/availability/payments and gallery — with a
not-published-yet note.
- Wired into CraftProfileForm (8 crafts) and the coach + filmer/editor/
photographer/builder pages via a Preview button next to Submit.
- parseLabExtras now accepts any { tags } so a draft tag list can be previewed
without a published event.
- i18n for the preview copy (EN/ES).
Still open: draft/autosave and a multi-step layout.
215 lines
8.2 KiB
TypeScript
215 lines
8.2 KiB
TypeScript
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
} from '@/components/ui/dialog';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { ImageWithFallback } from '@/components/ImageWithFallback';
|
||
import { useTranslation } from '@/contexts/language';
|
||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||
import {
|
||
parseLabExtras,
|
||
labOptionLabel,
|
||
PAYMENT_METHODS,
|
||
AVAILABILITY_OPTIONS,
|
||
RATE_CURRENCIES,
|
||
ALL_RATE_UNITS,
|
||
} from '@/lib/labExtras';
|
||
import { Banknote, Eye, Languages as LanguagesIcon, MapPin } from 'lucide-react';
|
||
|
||
export interface LabPreviewSkill {
|
||
label: string;
|
||
description?: string;
|
||
}
|
||
|
||
export interface LabPreviewDraft {
|
||
title: string;
|
||
bio: string;
|
||
image?: string | null;
|
||
location?: string;
|
||
experience?: string;
|
||
rate?: string;
|
||
skills?: LabPreviewSkill[];
|
||
contact?: {
|
||
phone?: string;
|
||
email?: string;
|
||
website?: string;
|
||
instagram?: string;
|
||
portfolio?: string;
|
||
social?: string;
|
||
noffer?: string;
|
||
};
|
||
/** The form's shared enrichment tags (languages/availability/rate/payments/photo). */
|
||
extraTags?: string[][];
|
||
}
|
||
|
||
interface LabProfilePreviewProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
draft: LabPreviewDraft;
|
||
heading: string;
|
||
noun: string;
|
||
accentText: string;
|
||
accentChip: string;
|
||
icon: React.ComponentType<{ className?: string }>;
|
||
}
|
||
|
||
/**
|
||
* A "how it will look" preview of a Lab provider profile, built from the form's
|
||
* current draft — the create flow is long and otherwise blind until publish.
|
||
* Deliberately a faithful *card*, not the full detail page (which fetches the
|
||
* published event).
|
||
*/
|
||
export function LabProfilePreview({
|
||
open,
|
||
onOpenChange,
|
||
draft,
|
||
heading,
|
||
noun,
|
||
accentText,
|
||
accentChip,
|
||
icon: Icon,
|
||
}: LabProfilePreviewProps) {
|
||
const { t } = useTranslation();
|
||
const extras = parseLabExtras({ tags: draft.extraTags ?? [] });
|
||
|
||
const rateAmount = extras.rateAmount;
|
||
const rateDisplay = rateAmount
|
||
? `${rateAmount} ${labOptionLabel(RATE_CURRENCIES, extras.rateCurrency ?? 'sats', t)}`
|
||
: draft.rate;
|
||
const unitKey = extras.rateUnit ? ALL_RATE_UNITS.find((u) => u.value === extras.rateUnit)?.key : undefined;
|
||
const contact = draft.contact ?? {};
|
||
const skills = (draft.skills ?? []).slice(0, 8);
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-h-[90vh] w-[calc(100vw-2rem)] max-w-lg overflow-y-auto bg-black border-white/10">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2 uppercase font-black">
|
||
<Eye className={`h-5 w-5 ${accentText}`} /> {t('labUi.preview.title')}
|
||
</DialogTitle>
|
||
<DialogDescription className="text-gray-400">
|
||
{t('labUi.preview.desc', { noun })}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="rounded-2xl border-2 border-white/10 bg-white/[0.03] p-4">
|
||
{/* Header */}
|
||
<div className="flex items-start gap-3">
|
||
<div className="h-16 w-16 flex-shrink-0 overflow-hidden rounded-2xl bg-white/5">
|
||
{draft.image ? (
|
||
<ImageWithFallback src={draft.image} alt={draft.title} className="h-full w-full object-cover" />
|
||
) : (
|
||
<div className="flex h-full w-full items-center justify-center">
|
||
<Icon className={`h-7 w-7 ${accentText}`} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<h3 className={`text-xl font-black uppercase ${accentText}`} style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}>
|
||
{draft.title || t('labUi.preview.untitled')}
|
||
</h3>
|
||
<Badge variant="outline" className={`gap-1 ${accentText}`}>
|
||
<Icon className="h-3 w-3" /> {heading}
|
||
</Badge>
|
||
</div>
|
||
{draft.location && (
|
||
<p className="mt-1 flex items-center gap-1 text-sm text-gray-400">
|
||
<MapPin className="h-3.5 w-3.5" /> {draft.location}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bio */}
|
||
<p className="mt-3 whitespace-pre-wrap text-sm text-gray-300">
|
||
{draft.bio || <span className="text-gray-600">{t('labUi.preview.noBio')}</span>}
|
||
</p>
|
||
|
||
{/* Skills */}
|
||
{skills.length > 0 && (
|
||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||
{skills.map((s) => (
|
||
<span key={s.label} className={`rounded-full border px-2.5 py-1 text-xs font-bold ${accentChip}`}>
|
||
{s.label}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Rate + experience */}
|
||
{(rateDisplay || draft.experience) && (
|
||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||
{rateDisplay && (
|
||
<span className="font-bold text-orange-400">
|
||
💰 {rateDisplay}{unitKey ? ` · ${t(unitKey)}` : ''}
|
||
</span>
|
||
)}
|
||
{draft.experience && (
|
||
<span className="text-gray-400">{t('labUi.preview.years', { count: draft.experience })}</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Languages / availability / payments */}
|
||
{(extras.languages.length > 0 || extras.availability.length > 0 || extras.paymentMethods.length > 0 || extras.payWhatYouCan) && (
|
||
<div className="mt-3 space-y-2">
|
||
{extras.languages.length > 0 && (
|
||
<p className="flex flex-wrap items-center gap-2 text-xs text-gray-400">
|
||
<LanguagesIcon className="h-3.5 w-3.5" /> {extras.languages.join(', ')}
|
||
</p>
|
||
)}
|
||
{extras.availability.length > 0 && (
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{extras.availability.map((v) => (
|
||
<span key={v} className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[11px] text-gray-400">
|
||
{labOptionLabel(AVAILABILITY_OPTIONS, v, t)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
{(extras.paymentMethods.length > 0 || extras.payWhatYouCan) && (
|
||
<div className="flex flex-wrap items-center gap-1.5 text-xs text-gray-400">
|
||
<Banknote className="h-3.5 w-3.5" />
|
||
{extras.paymentMethods.map((m) => (
|
||
<span key={m} className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5">{labOptionLabel(PAYMENT_METHODS, m, t)}</span>
|
||
))}
|
||
{extras.payWhatYouCan && (
|
||
<span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5">{t('labUi.detail.payWhatYouCan')}</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Gallery preview */}
|
||
{extras.photos.length > 0 && (
|
||
<div className="mt-3 grid grid-cols-4 gap-1.5">
|
||
{extras.photos.slice(0, 8).map((url) => (
|
||
<div key={url} className="aspect-square overflow-hidden rounded-lg bg-white/5">
|
||
<ImageWithFallback src={url} alt="" className="h-full w-full object-cover" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Contact */}
|
||
{(sanitizeUrl(contact.website) || sanitizeUrl(contact.portfolio) || sanitizeUrl(contact.social) || contact.email || contact.phone || contact.instagram) && (
|
||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-gray-400">
|
||
{contact.phone && <span>📞 {contact.phone}</span>}
|
||
{contact.email && <span>✉️ {contact.email}</span>}
|
||
{sanitizeUrl(contact.website) && <span>🔗 {t('labUi.become.website')}</span>}
|
||
{contact.instagram && <span>📷 {contact.instagram}</span>}
|
||
{sanitizeUrl(contact.portfolio) && <span>🎨 {t('labUi.preview.portfolio')}</span>}
|
||
</div>
|
||
)}
|
||
|
||
<p className="mt-4 border-t border-white/10 pt-3 text-[11px] text-gray-500">{t('labUi.preview.note')}</p>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|