- Add all shadcn/ui components (button, card, form, dialog, navigation-menu, etc.) - Create main layout with navigation header and footer - Implement homepage with hero, showreel, featured projects, and capabilities - Build about page with studio origins, values, and capabilities - Create services page with detailed service offerings - Implement portfolio page with masonry grid for varying aspect ratios - Build contact page with 4-step multistep form - Add project and service data structures with placeholder content - Configure SEO metadata, canonical links, and JSON-LD schema - Add font preloading and image lazy-loading for performance - Configure Next.js Image for Unsplash remote patterns - Fix Navigation component to use modern Link pattern (remove legacyBehavior) - Add comprehensive README with project documentation
89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Image from "next/image";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Play } from "lucide-react";
|
|
import type { Project } from "@/data/projects";
|
|
|
|
interface ProjectCardProps {
|
|
project: Project;
|
|
}
|
|
|
|
export function ProjectCard({ project }: ProjectCardProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogTrigger asChild>
|
|
<Card className="group cursor-pointer overflow-hidden transition-all hover:shadow-lg">
|
|
<div className="relative overflow-hidden">
|
|
<Image
|
|
src={project.thumbnailUrl}
|
|
alt={project.title}
|
|
width={800}
|
|
height={800 / project.aspectRatio}
|
|
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
{project.videoUrl && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/0 transition-all group-hover:bg-black/40">
|
|
<Play className="h-12 w-12 text-white opacity-0 transition-opacity group-hover:opacity-100" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<CardTitle className="line-clamp-1">{project.title}</CardTitle>
|
|
<Badge variant="secondary">{project.category}</Badge>
|
|
</div>
|
|
<CardDescription className="line-clamp-2">
|
|
{project.description}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-2">
|
|
{project.tags.map((tag) => (
|
|
<Badge key={tag} variant="outline">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</DialogTrigger>
|
|
{project.videoUrl && (
|
|
<DialogContent className="max-w-5xl p-0">
|
|
<DialogHeader className="sr-only">
|
|
<DialogTitle>{project.title}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="aspect-video w-full">
|
|
<iframe
|
|
src={project.videoUrl}
|
|
title={project.title}
|
|
className="h-full w-full"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
/>
|
|
</div>
|
|
</DialogContent>
|
|
)}
|
|
</Dialog>
|
|
);
|
|
}
|
|
|