mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 09:50:33 -04:00
35905e70d5
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Logo field now accepts/editable logo URL, plus a new storage-based Logo Picker to select or remove images from project storage. * Full storage picker: browse buckets, columns/list views, search, drag‑and‑drop uploads, file previews (image/audio/video), and single-file selection with responsive mobile/desktop layouts. * **Refactor** * Logo submission streamlined to send the provided URL directly (legacy file-read/upload flow removed). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
192 lines
6.4 KiB
TypeScript
192 lines
6.4 KiB
TypeScript
import { useParams } from 'common'
|
|
import { AlertCircle, LoaderCircle, X } 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">
|
|
<LoaderCircle 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
|
|
size="tiny"
|
|
onClick={() => onSelect(previewUrl!)}
|
|
disabled={!previewUrl}
|
|
loading={isLoading}
|
|
>
|
|
Select
|
|
</Button>
|
|
<div className="text-foreground-lighter transition-colors hover:text-foreground">
|
|
<X
|
|
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">
|
|
<AlertCircle 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>
|
|
)
|
|
}
|