← back to IWasCute
src/components/licensor/ReviewSubmit.tsx
360 lines
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
ChevronLeft,
Camera,
Copyright,
ShieldCheck,
CheckCircle2,
Loader2,
AlertCircle,
ExternalLink,
} from 'lucide-react'
import clsx from 'clsx'
import type { PhotoFile } from './PhotoUpload'
import type { CopyrightData } from './CopyrightInfo'
import type { LikenessData } from './LikenessRelease'
interface ReviewSubmitProps {
photos: PhotoFile[]
copyrightInfo: CopyrightData
likenessRelease: LikenessData
onBack: () => void
}
type SubmitState = 'idle' | 'submitting' | 'success' | 'error'
function SectionCard({
icon,
title,
children,
}: {
icon: React.ReactNode
title: string
children: React.ReactNode
}) {
return (
<div
className="rounded-3xl overflow-hidden"
style={{ border: '1px solid var(--color-cream-dark)', background: 'rgba(255,255,255,0.6)' }}
>
<div
className="flex items-center gap-3 px-6 py-4 border-b"
style={{ borderColor: 'var(--color-cream-dark)', background: 'rgba(255,255,255,0.4)' }}
>
<div
className="w-8 h-8 rounded-xl flex items-center justify-center"
style={{ background: 'var(--color-peach)' }}
>
{icon}
</div>
<h3 className="text-sm font-bold tracking-wide" style={{ color: 'var(--color-brown)' }}>
{title}
</h3>
</div>
<div className="px-6 py-5">{children}</div>
</div>
)
}
function ReviewRow({ label, value }: { label: string; value: string | React.ReactNode }) {
return (
<div className="flex items-start justify-between gap-4 py-2.5 border-b last:border-b-0" style={{ borderColor: 'var(--color-cream-dark)' }}>
<span className="text-sm shrink-0" style={{ color: 'var(--color-warm-gray)' }}>
{label}
</span>
<span className="text-sm font-medium text-right" style={{ color: 'var(--color-brown)' }}>
{value}
</span>
</div>
)
}
const RELATIONSHIP_LABELS: Record<string, string> = {
parent: 'My parent',
family: 'Other family member',
professional: 'Professional photographer',
unknown: "Don't know",
}
const PERMISSION_LABELS: Record<string, string> = {
yes: 'Yes — has permission',
no: 'No explicit permission',
deceased: 'Photographer deceased',
}
export default function ReviewSubmit({
photos,
copyrightInfo,
likenessRelease,
onBack,
}: ReviewSubmitProps) {
const [submitState, setSubmitState] = useState<SubmitState>('idle')
const [errorMsg, setErrorMsg] = useState('')
const usageTypes = [
likenessRelease.editorial && 'Editorial',
likenessRelease.commercial && 'Commercial',
likenessRelease.aiTraining && 'AI Training',
]
.filter(Boolean)
.join(', ')
const handleSubmit = async () => {
setSubmitState('submitting')
setErrorMsg('')
// Simulated async submission
await new Promise((r) => setTimeout(r, 2200))
// In production: POST to /api/licensor/submit with FormData
const success = Math.random() > 0.05 // 95% success rate for demo
if (success) {
setSubmitState('success')
} else {
setSubmitState('error')
setErrorMsg('Something went wrong. Please try again.')
}
}
if (submitState === 'success') {
return (
<motion.div
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 28 }}
className="flex flex-col items-center justify-center gap-8 py-16 text-center"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 22, delay: 0.1 }}
className="w-24 h-24 rounded-full flex items-center justify-center"
style={{ background: 'var(--color-sage)' }}
>
<CheckCircle2 size={48} style={{ color: 'white' }} />
</motion.div>
<div className="flex flex-col gap-3 max-w-md">
<h2 className="text-3xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
You're in the queue!
</h2>
<p className="text-base leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
Your {photos.length} photo{photos.length !== 1 ? 's' : ''}{' '}
{photos.length !== 1 ? 'have' : 'has'} been submitted for rights review. We'll
notify you by email when they're cleared for licensing.
</p>
</div>
<div
className="w-full max-w-sm rounded-3xl p-6 flex flex-col gap-3"
style={{ background: 'var(--color-cream-dark)' }}
>
<p className="text-xs font-bold tracking-widest uppercase" style={{ color: 'var(--color-warm-gray)' }}>
What happens next
</p>
{[
{ step: '1', text: 'Rights review (24–48 hours)' },
{ step: '2', text: 'Copyright clearance notification' },
{ step: '3', text: 'Photos go live in marketplace' },
{ step: '4', text: 'Royalties deposited when licensed' },
].map(({ step, text }) => (
<div key={step} className="flex items-center gap-3">
<div
className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 text-xs font-bold"
style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
>
{step}
</div>
<p className="text-sm" style={{ color: 'var(--color-brown)' }}>
{text}
</p>
</div>
))}
</div>
<a
href="/dashboard"
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
>
Go to My Dashboard
<ExternalLink size={14} />
</a>
</motion.div>
)
}
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div>
<h2
className="text-2xl md:text-3xl font-bold tracking-tight"
style={{ color: 'var(--color-brown)' }}
>
Review & Submit
</h2>
<p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
Please review everything below before submitting for rights review.
</p>
</div>
{/* Photos section */}
<SectionCard icon={<Camera size={16} style={{ color: 'var(--color-brown)' }} />} title={`${photos.length} Photo${photos.length !== 1 ? 's' : ''}`}>
<div
className="flex gap-2 flex-wrap"
>
{photos.map((photo, i) => (
<div
key={photo.id}
className="relative rounded-xl overflow-hidden"
style={{ width: 72, height: 72, flexShrink: 0 }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={photo.preview}
alt={`Photo ${i + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
</SectionCard>
{/* Copyright section */}
<SectionCard
icon={<Copyright size={16} style={{ color: 'var(--color-brown)' }} />}
title="Copyright Information"
>
<ReviewRow
label="Photographer"
value={RELATIONSHIP_LABELS[copyrightInfo.relationship] ?? copyrightInfo.relationship}
/>
{copyrightInfo.photographer && (
<ReviewRow label="Name" value={copyrightInfo.photographer} />
)}
{copyrightInfo.year && (
<ReviewRow label="Year taken" value={copyrightInfo.year} />
)}
<ReviewRow
label="Permission"
value={PERMISSION_LABELS[copyrightInfo.hasPermission] ?? copyrightInfo.hasPermission}
/>
{copyrightInfo.hasPermission === 'deceased' && (
<ReviewRow
label="Estate rights"
value={copyrightInfo.estateRights ? 'Has evidence' : 'No evidence'}
/>
)}
</SectionCard>
{/* Likeness section */}
<SectionCard
icon={<ShieldCheck size={16} style={{ color: 'var(--color-brown)' }} />}
title="Likeness Release"
>
<ReviewRow label="Subject confirmed" value={likenessRelease.isSubject ? 'Yes' : 'No'} />
<ReviewRow label="Age confirmed (18+)" value={likenessRelease.isAdult ? 'Yes' : 'No'} />
<ReviewRow label="Licensed for" value={usageTypes || 'None selected'} />
<ReviewRow
label="Signed by"
value={
<span className="signature-field text-base">
{likenessRelease.signature}
</span>
}
/>
<ReviewRow label="Date" value={likenessRelease.date} />
</SectionCard>
{/* Legal notice */}
<div
className="flex items-start gap-3 p-4 rounded-2xl text-xs leading-relaxed"
style={{ background: 'rgba(61, 64, 91, 0.05)', border: '1px solid var(--color-cream-dark)' }}
role="note"
>
<AlertCircle size={14} className="shrink-0 mt-0.5" style={{ color: 'var(--color-warm-gray)' }} />
<p style={{ color: 'var(--color-warm-gray)' }}>
By submitting, you certify that all information above is accurate and that you have the
legal authority to license these photos. False submissions may result in account termination
and legal liability.
</p>
</div>
{/* Error */}
<AnimatePresence>
{submitState === 'error' && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
className="flex items-center gap-3 p-4 rounded-2xl"
style={{ background: 'rgba(255, 181, 167, 0.2)', border: '1px solid var(--color-peach)' }}
role="alert"
>
<AlertCircle size={16} style={{ color: 'var(--color-peach-dark)' }} />
<p className="text-sm font-medium" style={{ color: 'var(--color-brown)' }}>
{errorMsg}
</p>
</motion.div>
)}
</AnimatePresence>
{/* Navigation */}
<div className="flex items-center justify-between pt-2">
<button
onClick={onBack}
disabled={submitState === 'submitting'}
className={clsx(
'flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide',
'transition-all hover:scale-105 disabled:opacity-40 disabled:cursor-not-allowed'
)}
style={{
background: 'var(--color-cream-dark)',
color: 'var(--color-warm-gray)',
}}
>
<ChevronLeft size={16} />
Back
</button>
<motion.button
whileHover={submitState === 'idle' ? { scale: 1.03 } : {}}
whileTap={submitState === 'idle' ? { scale: 0.97 } : {}}
onClick={handleSubmit}
disabled={submitState === 'submitting'}
className={clsx(
'flex items-center gap-2.5 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide',
'transition-all duration-200',
submitState === 'submitting' && 'cursor-not-allowed'
)}
style={{
background:
submitState === 'error'
? 'var(--color-peach-dark)'
: 'var(--color-peach)',
color: 'var(--color-brown)',
}}
>
{submitState === 'submitting' ? (
<>
<Loader2 size={16} className="animate-spin" />
Submitting…
</>
) : submitState === 'error' ? (
'Try Again'
) : (
<>
<CheckCircle2 size={16} />
Submit for Review
</>
)}
</motion.button>
</div>
</div>
)
}