Add timelapse to single image
This commit is contained in:
@ -48,6 +48,7 @@ model Artwork {
|
|||||||
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
gallery Gallery? @relation(fields: [galleryId], references: [id])
|
||||||
|
|
||||||
metadata ArtworkMetadata?
|
metadata ArtworkMetadata?
|
||||||
|
timelapse ArtworkTimelapse?
|
||||||
|
|
||||||
albums Album[]
|
albums Album[]
|
||||||
categories ArtCategory[]
|
categories ArtCategory[]
|
||||||
@ -195,6 +196,22 @@ model ArtworkMetadata {
|
|||||||
artwork Artwork @relation(fields: [artworkId], references: [id])
|
artwork Artwork @relation(fields: [artworkId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ArtworkTimelapse {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
artworkId String @unique
|
||||||
|
artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
enabled Boolean @default(false)
|
||||||
|
|
||||||
|
s3Key String @unique
|
||||||
|
fileName String?
|
||||||
|
mimeType String?
|
||||||
|
sizeBytes Int?
|
||||||
|
}
|
||||||
|
|
||||||
model FileData {
|
model FileData {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -353,14 +370,14 @@ model CommissionTypeCustomInput {
|
|||||||
|
|
||||||
model CommissionRequest {
|
model CommissionRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
index Int @default(autoincrement())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
sortIndex Int @default(0)
|
|
||||||
|
|
||||||
customerName String
|
customerName String
|
||||||
customerEmail String
|
customerEmail String
|
||||||
message String
|
message String
|
||||||
status String @default("NEW") // NEW | REVIEWING | ACCEPTED | REJECTED | SPAM
|
status String @default("NEW")
|
||||||
|
|
||||||
customerSocials String?
|
customerSocials String?
|
||||||
ipAddress String?
|
ipAddress String?
|
||||||
@ -422,6 +439,11 @@ model User {
|
|||||||
sessions Session[]
|
sessions Session[]
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
|
|
||||||
|
role String @default("user")
|
||||||
|
banned Boolean? @default(false)
|
||||||
|
banReason String?
|
||||||
|
banExpires DateTime?
|
||||||
|
|
||||||
@@unique([email])
|
@@unique([email])
|
||||||
@@map("user")
|
@@map("user")
|
||||||
}
|
}
|
||||||
@ -437,6 +459,8 @@ model Session {
|
|||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
impersonatedBy String?
|
||||||
|
|
||||||
@@unique([token])
|
@@unique([token])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@map("session")
|
@@map("session")
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard";
|
import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard";
|
||||||
|
import ArtworkTimelapseViewer from "@/components/artworks/ArtworkTimelapseViewer";
|
||||||
import { ContextBackButton } from "@/components/artworks/ContextBackButton";
|
import { ContextBackButton } from "@/components/artworks/ContextBackButton";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { PlayCircle } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@ -19,7 +22,8 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
categories: true,
|
categories: true,
|
||||||
colors: { include: { color: true } },
|
colors: { include: { color: true } },
|
||||||
tags: true,
|
tags: true,
|
||||||
variants: true
|
variants: true,
|
||||||
|
timelapse: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -62,6 +66,20 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{artwork.timelapse?.enabled ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<ArtworkTimelapseViewer
|
||||||
|
timelapse={artwork.timelapse}
|
||||||
|
artworkName={artwork.name}
|
||||||
|
trigger={
|
||||||
|
<Button size="lg" className="gap-2">
|
||||||
|
<PlayCircle className="h-5 w-5" />
|
||||||
|
Watch timelapse
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
71
src/components/artworks/ArtworkTimelapseViewer.tsx
Normal file
71
src/components/artworks/ArtworkTimelapseViewer.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type Timelapse = {
|
||||||
|
s3Key: string;
|
||||||
|
fileName: string | null;
|
||||||
|
mimeType: string | null;
|
||||||
|
sizeBytes: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ArtworkTimelapseViewer({
|
||||||
|
timelapse,
|
||||||
|
artworkName,
|
||||||
|
trigger,
|
||||||
|
}: {
|
||||||
|
timelapse: Timelapse;
|
||||||
|
artworkName?: string | null;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
// IMPORTANT:
|
||||||
|
// This assumes your existing `/api/image/[...key]` can stream arbitrary S3 keys.
|
||||||
|
// If your route expects a different format, adjust this in one place.
|
||||||
|
const src = `/api/image/${encodeURI(timelapse.s3Key)}`;
|
||||||
|
|
||||||
|
// Minimal empty captions track (satisfies jsx-a11y/media-has-caption)
|
||||||
|
const emptyVtt = "data:text/vtt;charset=utf-8,WEBVTT%0A%0A";
|
||||||
|
|
||||||
|
const title = artworkName ? `Timelapse — ${artworkName}` : "Timelapse";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Only render video when open (prevents unnecessary network / CPU). */}
|
||||||
|
{open ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<video
|
||||||
|
className="w-full rounded-md border bg-black"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
playsInline
|
||||||
|
>
|
||||||
|
<source src={src} type={timelapse.mimeType ?? "video/mp4"} />
|
||||||
|
<track kind="captions" src={emptyVtt} srcLang="en" label="Captions" default />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{timelapse.fileName ? timelapse.fileName : timelapse.s3Key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user