Setting the file. One moment.
Project Detail View · Wix Headless Fast · wix/skills · Skills Docs
ContentsBack to the top of the page references/portfolio/app/components/portfolio/ProjectDetailView.tsx
references/portfolio/app/components/portfolio/ ProjectDetailView.tsx
TypeScript · 115 lines · 4 KB
export
function
GalleryMedia
({
item
}
:
{
item
:
GalleryItem
}) {
11 if (item.kind === "video" && item.videoUrl) {
12 return (
13 < video
14 src ={ item.videoUrl }
15 poster ={ item.imageUrl || undefined }
16 controls
17 playsInline
18 className = "block w-full rounded-lg bg-secondary"
19 />
20 );
21 }
22 if (item.imageUrl) {
23 return (
24 < img src ={ item.imageUrl } alt ={ item.title } loading = "lazy" className = "block w-full rounded-lg" />
25 );
26 }
27 return null ;
28 }
29
30 function MaybeLink ({ item , children } : { item : GalleryItem ; children : ReactNode }) {
31 return item.linkUrl ? (
32 < a href ={ item.linkUrl } target ={ item.linkTarget ?? undefined } rel = "noopener" >
33 { children }
34 </ a >
35 ) : (
36 <> { children } </>
37 );
38 }
39
40 export interface ProjectDetailViewProps {
41 slug : string ;
42 initialProject ?: ProjectDetail ;
43 initialItems ?: GalleryItem [];
44 }
45
46 export default function ProjectDetailView ({ slug, initialProject, initialItems } : ProjectDetailViewProps) {
47 const { project , notFound , items , error } = useProjectDetail (slug, { initialProject, initialItems });
48
49 if (notFound) {
50 return < p className = "py-16 text-center text-muted-foreground" >Project not found.</ p >;
51 }
52 if ( ! project) {
53 return (
54 < div aria-busy = "true" >
55 < div className = "h-7 w-64 animate-pulse rounded bg-secondary" />
56 < div className = "mt-6 aspect-[4/3] animate-pulse rounded-lg bg-secondary" />
57 </ div >
58 );
59 }
60
61 return (
62 < article >
63 < header className = "mb-8 max-w-2xl" >
64 < h1 className = "text-2xl font-semibold tracking-tight" > { project.title } </ h1 >
65 { project.description && (
66 < p className = "mt-2 leading-relaxed text-muted-foreground" > { project.description } </ p >
67 ) }
68 { project.details. length > 0 && (
69 < dl className = "mt-5 grid grid-cols-[auto_1fr] items-baseline gap-x-6 gap-y-1.5" >
70 { project.details. map (( d , i ) => (
71 < div key ={ i } className = "contents" >
72 < dt className = "text-sm text-muted-foreground" > { d.label } </ dt >
73 < dd className = "m-0 text-sm text-foreground" >
74 { d.url ? (
75 < a href ={ d.url } target ={ d.target ?? undefined } rel = "noopener" className = "underline" >
76 { d.text }
77 </ a >
78 ) : (
79 d.text
80 ) }
81 </ dd >
82 </ div >
83 )) }
84 </ dl >
85 ) }
86 </ header >
87 { error && < p className = "mb-4 text-sm text-red-600" > { error } </ p > }
88 { items === null ? (
89 < div className = "flex flex-col gap-6" aria-busy = "true" >
90 < div className = "aspect-[4/3] animate-pulse rounded-lg bg-secondary" />
91 </ div >
92 ) : items. length === 0 ? (
93 // No gallery items — the cover is the project's only real media; text-only otherwise.
94 project.imageUrl ? (
95 < img src ={ project.imageUrl } alt ={ project.title } className = "block w-full rounded-lg" />
96 ) : (
97 < p className = "py-16 text-center text-muted-foreground" >No media in this project yet.</ p >
98 )
99 ) : (
100 < div className = "flex flex-col gap-6" >
101 { items. map (( item ) => (
102 < figure key ={ item.id } className = "m-0" >
103 < MaybeLink item ={ item } >
104 < GalleryMedia item ={ item } />
105 </ MaybeLink >
106 { item.title && (
107 < figcaption className = "mt-1.5 text-sm text-muted-foreground" > { item.title } </ figcaption >
108 ) }
109 </ figure >
110 )) }
111 </ div >
112 ) }
113 </ article >
114 );
115 }