Files
simple-react-app-kit/packages/media/src/media-detail.tsx
T

192 lines
5.8 KiB
TypeScript
Raw Normal View History

import * as React from "react"
import {
ExternalLinkIcon,
FilmIcon,
HeartIcon,
ImageIcon,
Trash2Icon,
} from "@workspace/ui/components/icon"
import { useTranslate } from "@workspace/i18n"
import { Button } from "@workspace/ui/components/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@workspace/ui/components/dialog"
import { Separator } from "@workspace/ui/components/separator"
import { cn } from "@workspace/ui/lib/utils"
import { mediaMessages } from "./messages"
import type { MediaAsset } from "./types"
import { formatMediaFileSize } from "./utils"
export interface MediaDetailDialogProps {
asset?: MediaAsset
deletePending?: boolean
favoritePending?: boolean
open: boolean
resolveUrl: (asset: MediaAsset) => string
onDelete: (asset: MediaAsset) => void
onFavorite: (asset: MediaAsset) => void
onOpenChange: (open: boolean) => void
}
export function MediaDetailDialog({
asset,
deletePending,
favoritePending,
open,
resolveUrl,
onDelete,
onFavorite,
onOpenChange,
}: MediaDetailDialogProps) {
const t = useTranslate()
const [zoom, setZoom] = React.useState(1)
const imageSource = asset?.kind === "image" ? resolveUrl(asset) : undefined
React.useEffect(() => setZoom(1), [imageSource, open])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{t(mediaMessages.details)}</DialogTitle>
<DialogDescription className="truncate">
{asset?.name}
</DialogDescription>
</DialogHeader>
{asset && (
<>
<div className="grid max-h-[55dvh] min-h-56 place-items-center overflow-hidden rounded-lg bg-muted/50">
{asset.kind === "image" ? (
<img
src={imageSource}
alt={asset.name}
draggable={false}
className="max-h-[55dvh] max-w-full touch-none object-contain select-none"
style={{ transform: `scale(${zoom})` }}
onWheel={(event) => {
event.preventDefault()
setZoom((current) =>
Math.min(4, Math.max(1, current - event.deltaY / 400))
)
}}
onDoubleClick={() => setZoom(1)}
/>
) : (
<video
src={resolveUrl(asset)}
controls
preload="metadata"
className="max-h-[55dvh] max-w-full bg-black object-contain"
/>
)}
</div>
<div className="flex min-w-0 items-center gap-2">
{asset.kind === "image" ? (
<ImageIcon className="size-4 shrink-0" />
) : (
<FilmIcon className="size-4 shrink-0" />
)}
<strong className="truncate">{asset.name}</strong>
</div>
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-[repeat(3,minmax(0,1fr))_auto]">
<MediaMetadata
label={t(mediaMessages.type)}
value={asset.mimeType}
/>
<MediaMetadata
label={t(mediaMessages.size)}
value={formatMediaFileSize(asset.sizeBytes)}
/>
<MediaMetadata
label={t(mediaMessages.dimensions)}
value={
asset.width && asset.height
? `${asset.width} × ${asset.height}`
: t(mediaMessages.noDimensions)
}
/>
<MediaMetadata
label={t(mediaMessages.fileName)}
value={asset.name}
/>
<MediaMetadata
label={t(mediaMessages.uploadedAt)}
value={new Date(asset.createdAt).toLocaleString()}
/>
</dl>
<Separator />
<DialogFooter className="flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Button
type="button"
variant="outline"
disabled={favoritePending}
onClick={() => onFavorite(asset)}
>
<HeartIcon
className={asset.favorite ? "fill-current" : undefined}
/>
{asset.favorite
? t(mediaMessages.unfavorite)
: t(mediaMessages.favoriteAction)}
</Button>
<div className="flex flex-col gap-2 sm:flex-row">
<Button
nativeButton={false}
variant="outline"
render={
<a
href={resolveUrl(asset)}
target="_blank"
rel="noreferrer"
/>
}
>
<ExternalLinkIcon />
{t(mediaMessages.viewOriginal)}
</Button>
<Button
type="button"
variant="destructive"
disabled={deletePending}
onClick={() => onDelete(asset)}
>
<Trash2Icon />
{t(mediaMessages.delete)}
</Button>
</div>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
function MediaMetadata({
label,
value,
}: {
label: string
value: React.ReactNode
}) {
return (
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className={cn("mt-1 font-medium wrap-break-word tabular-nums")}>
{value}
</dd>
</div>
)
}