Files
supabase/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerPreviewPane.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

197 lines
6.4 KiB
TypeScript

import { useParams } from 'common'
import { AlertCircleIcon, LoaderCircleIcon, XIcon } from 'lucide-react'
import SVG from 'react-inlinesvg'
import { Button } from 'ui'
import { StorageItemWithColumn } from '../Storage.types'
import { useFetchFileUrlQuery } from '../StorageExplorer/useFetchFileUrlQuery'
import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState'
import { BASE_PATH } from '@/lib/constants'
import { formatBytes } from '@/lib/helpers'
const PREVIEW_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
const PreviewFile = ({ item }: { item: StorageItemWithColumn }) => {
const { ref: projectRef } = useParams()
const { bucket, columns } = useBucketFilePickerStateSnapshot()
const path = columns.slice(0, item.columnIndex).concat(item.name).join('/')
const { data: previewUrl, isPending: isLoading } = useFetchFileUrlQuery({
path,
projectRef: projectRef!,
bucket,
})
// if the size is not available, we set it to be greater than the max size
const size = +(item.metadata?.size ?? PREVIEW_SIZE_LIMIT + 1)
const mimeType = item.metadata?.mimetype
const isSkipped = !!mimeType && !!size && size > PREVIEW_SIZE_LIMIT
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center text-foreground-lighter">
<LoaderCircleIcon
size={14}
strokeWidth={2}
className="animate-spin text-foreground-lighter"
/>
</div>
)
}
if (isSkipped) {
return (
<div className="flex h-full w-full flex-col items-center justify-center">
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
<p className="mt-2 w-2/5 text-center text-sm">
File size is too large to preview in the explorer
</p>
</div>
)
}
if (!mimeType || !previewUrl) {
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
if (mimeType.includes('image')) {
return (
<div
className="flex h-full w-full items-center justify-center bg-contain bg-center bg-no-repeat"
style={{ backgroundImage: `url('${previewUrl}')` }}
/>
)
}
if (mimeType.includes('audio')) {
return (
<div className="flex h-full w-full items-center justify-center px-10">
<audio key={previewUrl} controls style={{ width: 'inherit' }}>
<source src={previewUrl} type="audio/mpeg" />
<p className="text-sm text-foreground-light">
Your browser does not support the audio element.
</p>
</audio>
</div>
)
}
if (mimeType.includes('video')) {
return (
<div className="flex h-full w-full items-center justify-center">
<video key={previewUrl} controls style={{ maxHeight: '100%' }}>
<source src={previewUrl} type="video/mp4" />
<p className="text-sm text-foreground-light">
Your browser does not support the video tag.
</p>
</video>
</div>
)
}
return (
<SVG
src={`${BASE_PATH}/img/file-filled.svg`}
preProcessor={(code) =>
code.replace(/svg/, 'svg class="mx-auto w-32 h-32 text-color-inherit opacity-75"')
}
/>
)
}
export const PreviewPane = ({ onSelect }: { onSelect: (url: string) => void }) => {
const { ref: projectRef } = useParams()
const {
selectedFilePreview: file,
setSelectedFilePreview,
bucket,
columns,
} = useBucketFilePickerStateSnapshot()
const path = file ? columns.slice(0, file.columnIndex).concat(file.name).join('/') : ''
const { data: previewUrl, isLoading } = useFetchFileUrlQuery(
{ path, projectRef: projectRef!, bucket },
{ enabled: !!file }
)
if (!file) return null
const size = file.metadata ? formatBytes(file.metadata.size) : null
const mimeType = file.metadata ? file.metadata.mimetype : undefined
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown'
const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown'
return (
<div className="h-full border-l border-overlay bg-surface-100 overflow-y-auto w-[450px] pb-4">
{/* Preview Header */}
<div className="flex w-full justify-end items-center gap-2 sticky top-0 bg-surface-100 p-4 border-b">
<Button
variant="primary"
size="tiny"
onClick={() => onSelect(previewUrl!)}
disabled={!previewUrl}
loading={isLoading}
>
Select
</Button>
<div className="text-foreground-lighter transition-colors hover:text-foreground">
<XIcon
className="cursor-pointer"
size={14}
strokeWidth={2}
onClick={() => setSelectedFilePreview(undefined)}
/>
</div>
</div>
{/* Preview Thumbnail*/}
<div className="my-4 border border-overlay mx-4">
<div className="flex h-56 w-full items-center 2xl:h-72">
<PreviewFile item={file} />
</div>
</div>
<div className="w-full space-y-6 px-4">
{/* Preview Information */}
<div className="space-y-1">
<h5 className="wrap-break-word text-base text-foreground">{file.name}</h5>
{file.isCorrupted && (
<div className="flex items-center space-x-2">
<AlertCircleIcon size={14} strokeWidth={2} className="text-foreground-light" />
<p className="text-sm text-foreground-light">
File is corrupted, please delete and reupload this file again
</p>
</div>
)}
{mimeType && (
<p className="text-sm text-foreground-light">
{mimeType}
{size && <span> - {size}</span>}
</p>
)}
</div>
{/* Preview Metadata */}
<div className="space-y-2">
<div>
<label className="mb-1 text-xs text-foreground-lighter">Added on</label>
<p className="text-sm text-foreground-light">{createdAt}</p>
</div>
<div>
<label className="mb-1 text-xs text-foreground-lighter">Last modified</label>
<p className="text-sm text-foreground-light">{updatedAt}</p>
</div>
</div>
</div>
</div>
)
}