feat(media): extract workspace package
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user