| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { useEffect, useState } from 'react' |
|
|
| export type ViewAngle = 'front' | 'left' | 'right' | 'back' |
|
|
| const VIEW_ANGLE_LABELS: Record<ViewAngle, string> = { |
| front: 'Front', |
| left: 'Left', |
| right: 'Right', |
| back: 'Back', |
| } |
|
|
| export interface ViewPackViewerProps { |
| viewPack: Partial<Record<ViewAngle, string>> |
| availableViews: ViewAngle[] |
| |
| initialAngle?: ViewAngle |
| |
| onAngleChange?: (angle: ViewAngle, url: string) => void |
| |
| onImageClick?: (url: string) => void |
| |
| imageClassName?: string |
| containerClassName?: string |
| } |
|
|
| export function ViewPackViewer({ |
| viewPack, |
| availableViews, |
| initialAngle, |
| onAngleChange, |
| onImageClick, |
| imageClassName, |
| containerClassName, |
| }: ViewPackViewerProps) { |
| const firstAvailable: ViewAngle = |
| initialAngle && viewPack[initialAngle] |
| ? initialAngle |
| : availableViews.find((a) => viewPack[a]) || 'front' |
|
|
| const [active, setActive] = useState<ViewAngle>(firstAvailable) |
|
|
| |
| useEffect(() => { |
| availableViews.forEach((angle) => { |
| const url = viewPack[angle] |
| if (url) { |
| const img = new Image() |
| img.src = url |
| } |
| }) |
| }, [viewPack, availableViews]) |
|
|
| |
| useEffect(() => { |
| if (!viewPack[active]) { |
| const fallback = availableViews.find((a) => viewPack[a]) |
| if (fallback) setActive(fallback) |
| } |
| }, [viewPack, availableViews, active]) |
|
|
| const activeUrl = viewPack[active] || '' |
| if (!activeUrl) return null |
|
|
| return ( |
| <div className={containerClassName ?? 'mt-3 hp-fade-in'}> |
| <img |
| src={activeUrl} |
| alt={`Persona ${VIEW_ANGLE_LABELS[active]} view`} |
| onClick={() => onImageClick?.(activeUrl)} |
| className={ |
| imageClassName ?? |
| 'w-72 max-h-96 h-auto object-contain rounded-xl border border-white/10 bg-black/20 cursor-zoom-in hover:opacity-90 transition-opacity' |
| } |
| onError={(e) => { |
| ;(e.target as HTMLImageElement).style.display = 'none' |
| }} |
| /> |
| <div className="flex gap-1.5 pt-2"> |
| {availableViews.map((angle) => { |
| const url = viewPack[angle] |
| if (!url) return null |
| const isActive = angle === active |
| return ( |
| <button |
| key={angle} |
| type="button" |
| onClick={() => { |
| setActive(angle) |
| onAngleChange?.(angle, url) |
| }} |
| className={`px-3 py-1 text-xs rounded-full border transition-colors ${ |
| isActive |
| ? 'bg-white/15 border-white/30 text-white/90 font-medium' |
| : 'bg-white/[0.04] border-white/10 text-white/50 hover:bg-white/10 hover:text-white/70' |
| }`} |
| aria-pressed={isActive} |
| > |
| {VIEW_ANGLE_LABELS[angle]} |
| </button> |
| ) |
| })} |
| </div> |
| </div> |
| ) |
| } |
|
|