← back to IWasCute
src/components/licensor/LikenessRelease.tsx
365 lines
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { ChevronLeft, ChevronRight, ShieldCheck, AlertTriangle } from 'lucide-react'
import clsx from 'clsx'
export interface LikenessData {
isSubject: boolean
isAdult: boolean
editorial: boolean
commercial: boolean
aiTraining: boolean
signature: string
date: string
}
interface LikenessReleaseProps {
likenessRelease: LikenessData
onLikenessChange: (data: LikenessData) => void
onNext: () => void
onBack: () => void
}
function CheckboxField({
id,
label,
description,
checked,
onChange,
required,
error,
}: {
id: string
label: string
description?: string
checked: boolean
onChange: (v: boolean) => void
required?: boolean
error?: boolean
}) {
return (
<label
htmlFor={id}
className="flex items-start gap-4 p-4 rounded-2xl cursor-pointer transition-all"
style={{
background: checked
? 'rgba(181, 213, 197, 0.2)'
: error
? 'rgba(255, 181, 167, 0.12)'
: 'rgba(255,255,255,0.5)',
border: `2px solid ${
checked
? 'var(--color-sage)'
: error
? 'var(--color-peach)'
: 'var(--color-cream-dark)'
}`,
}}
>
<div className="relative mt-0.5 shrink-0">
<input
id={id}
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
required={required}
className="sr-only"
aria-required={required}
/>
<div
className="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all"
style={{
background: checked ? 'var(--color-sage-dark)' : 'white',
borderColor: checked ? 'var(--color-sage-dark)' : 'var(--color-cream-dark)',
}}
aria-hidden
>
{checked && (
<motion.svg
initial={{ scale: 0 }}
animate={{ scale: 1 }}
width="11"
height="11"
viewBox="0 0 11 11"
fill="none"
>
<path
d="M2 5.5l2.5 2.5L9 3"
stroke="white"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</motion.svg>
)}
</div>
</div>
<div className="flex-1">
<p
className="text-sm font-semibold"
style={{ color: 'var(--color-brown)' }}
>
{label}
{required && (
<span className="ml-1" style={{ color: 'var(--color-peach-dark)' }} aria-hidden>
*
</span>
)}
</p>
{description && (
<p className="mt-0.5 text-xs leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
{description}
</p>
)}
</div>
</label>
)
}
export default function LikenessRelease({
likenessRelease,
onLikenessChange,
onNext,
onBack,
}: LikenessReleaseProps) {
const [attempted, setAttempted] = useState(false)
const patch = (updates: Partial<LikenessData>) => {
onLikenessChange({ ...likenessRelease, ...updates })
}
const atLeastOneUse =
likenessRelease.editorial ||
likenessRelease.commercial ||
likenessRelease.aiTraining
const canProceed =
likenessRelease.isSubject &&
likenessRelease.isAdult &&
atLeastOneUse &&
likenessRelease.signature.trim().length >= 2
const handleNext = () => {
setAttempted(true)
if (canProceed) onNext()
}
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)' }}
>
Likeness Release
</h2>
<p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
Confirm you are the person in these photos and grant licensing permissions.
</p>
</div>
{/* Security badge */}
<div
className="flex items-center gap-3 p-4 rounded-2xl"
style={{ background: 'rgba(181, 213, 197, 0.2)', border: '1px solid var(--color-sage)' }}
role="note"
>
<ShieldCheck size={20} style={{ color: 'var(--color-sage-dark)' }} />
<p className="text-sm" style={{ color: 'var(--color-warm-gray)' }}>
Your signature is stored securely and is legally binding. You can revoke permissions at
any time from your account.
</p>
</div>
{/* Identity confirmations */}
<section aria-labelledby="identity-heading">
<h3
id="identity-heading"
className="text-base font-bold tracking-wide mb-3"
style={{ color: 'var(--color-brown)' }}
>
Identity Confirmation
</h3>
<div className="flex flex-col gap-3">
<CheckboxField
id="is-subject"
label="I confirm I am the person depicted in these photos"
description="These are photos of me as a child. I am the subject whose likeness will be licensed."
checked={likenessRelease.isSubject}
onChange={(v) => patch({ isSubject: v })}
required
error={attempted && !likenessRelease.isSubject}
/>
<CheckboxField
id="is-adult"
label="I am 18 years of age or older"
description="You must be an adult to legally grant rights to your own likeness."
checked={likenessRelease.isAdult}
onChange={(v) => patch({ isAdult: v })}
required
error={attempted && !likenessRelease.isAdult}
/>
</div>
</section>
{/* Usage permissions */}
<section aria-labelledby="usage-heading">
<h3
id="usage-heading"
className="text-base font-bold tracking-wide mb-1"
style={{ color: 'var(--color-brown)' }}
>
I grant permission for these photos to be licensed for:
</h3>
<p className="text-xs mb-3" style={{ color: 'var(--color-warm-gray)' }}>
Select at least one use type. You earn royalties for each license granted.
</p>
{attempted && !atLeastOneUse && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-2 mb-3 p-3 rounded-xl"
style={{ background: 'rgba(255, 181, 167, 0.2)', border: '1px solid var(--color-peach)' }}
role="alert"
>
<AlertTriangle size={14} style={{ color: 'var(--color-peach-dark)' }} />
<p className="text-xs font-medium" style={{ color: 'var(--color-brown)' }}>
Please select at least one use type.
</p>
</motion.div>
)}
<div className="flex flex-col gap-3">
<CheckboxField
id="editorial"
label="Editorial use"
description="News articles, blog posts, non-commercial publications, and journalism."
checked={likenessRelease.editorial}
onChange={(v) => patch({ editorial: v })}
error={attempted && !atLeastOneUse}
/>
<CheckboxField
id="commercial"
label="Commercial use"
description="Advertisements, product packaging, marketing materials, and brand campaigns."
checked={likenessRelease.commercial}
onChange={(v) => patch({ commercial: v })}
error={attempted && !atLeastOneUse}
/>
<CheckboxField
id="ai-training"
label="AI training use"
description="Use in AI model training datasets. Higher royalty rate applies."
checked={likenessRelease.aiTraining}
onChange={(v) => patch({ aiTraining: v })}
error={attempted && !atLeastOneUse}
/>
</div>
</section>
{/* Signature */}
<section aria-labelledby="signature-heading">
<h3
id="signature-heading"
className="text-base font-bold tracking-wide mb-3"
style={{ color: 'var(--color-brown)' }}
>
Digital Signature
</h3>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label
htmlFor="signature"
className="text-sm font-semibold"
style={{ color: 'var(--color-brown)' }}
>
Type your full legal name to sign
<span className="ml-1" style={{ color: 'var(--color-peach-dark)' }} aria-hidden>
*
</span>
</label>
<input
id="signature"
type="text"
value={likenessRelease.signature}
onChange={(e) => patch({ signature: e.target.value })}
placeholder="Your full legal name"
autoComplete="name"
className={clsx(
'w-full px-5 py-4 rounded-2xl text-lg signature-field',
'border-2 outline-none transition-all duration-200'
)}
style={{
background: 'rgba(255,255,255,0.8)',
borderColor:
attempted && likenessRelease.signature.trim().length < 2
? 'var(--color-peach)'
: likenessRelease.signature.trim().length >= 2
? 'var(--color-sage)'
: 'var(--color-cream-dark)',
color: 'var(--color-brown)',
}}
aria-required="true"
aria-invalid={attempted && likenessRelease.signature.trim().length < 2}
/>
{attempted && likenessRelease.signature.trim().length < 2 && (
<p className="text-xs" style={{ color: 'var(--color-peach-dark)' }}>
Please enter your full legal name.
</p>
)}
</div>
{/* Date */}
<div
className="flex items-center justify-between px-5 py-3 rounded-2xl"
style={{ background: 'var(--color-cream-dark)' }}
>
<span className="text-sm font-medium" style={{ color: 'var(--color-warm-gray)' }}>
Date signed
</span>
<span className="text-sm font-semibold" style={{ color: 'var(--color-brown)' }}>
{likenessRelease.date}
</span>
</div>
</div>
</section>
{/* Navigation */}
<div className="flex items-center justify-between pt-2">
<button
onClick={onBack}
className="flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
style={{
background: 'var(--color-cream-dark)',
color: 'var(--color-warm-gray)',
}}
>
<ChevronLeft size={16} />
Back
</button>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={handleNext}
className="flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all duration-200"
style={{
background: 'var(--color-peach)',
color: 'var(--color-brown)',
opacity: canProceed ? 1 : 0.7,
}}
>
Review & Submit
<ChevronRight size={16} />
</motion.button>
</div>
</div>
)
}