mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
assets and tier plans revamp
This commit is contained in:
@@ -9,7 +9,8 @@ import { ArrowLeft, ArrowRight, UploadCloud, X, FileVideo, FileText, Image, File
|
||||
|
||||
import { useAssets } from "@/contexts/AdminAssetsContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { MAX_ASSET_FILE_SIZE, MAX_ASSET_FILE_SIZE_LABEL } from "@/utils/assetUpload.util";
|
||||
import { MAX_ASSET_FILE_SIZE_SINGLE, MAX_ASSET_FILE_SIZE_SINGLE_LABEL } from "@/utils/assetUpload.util";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -83,7 +84,7 @@ function DropZone({ label, accept, file, onFile, onClear, error }) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(file.size / 1024).toFixed(1)} KB · {file.type}
|
||||
{formatFileSize(file.size)} · {file.type}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -157,8 +158,8 @@ export default function AddAsset() {
|
||||
const fileType = file ? resolveFileType(file.type) : null;
|
||||
|
||||
const setFile = (f) => {
|
||||
if (f.size > MAX_ASSET_FILE_SIZE) {
|
||||
setError("_file", { message: `File exceeds the ${MAX_ASSET_FILE_SIZE_LABEL} size limit.` });
|
||||
if (f.size > MAX_ASSET_FILE_SIZE_SINGLE) {
|
||||
setError("_file", { message: `File exceeds the ${MAX_ASSET_FILE_SIZE_SINGLE_LABEL} size limit.` });
|
||||
return;
|
||||
}
|
||||
fileRef.current = f;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useAssets } from "@/contexts/AdminAssetsContext";
|
||||
import { useUploadQueue } from "@/contexts/UploadQueueContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
@@ -29,12 +30,6 @@ function FileTypeIcon({ mime = "" }) {
|
||||
return <FileText className="h-8 w-8 text-orange-400 shrink-0" />;
|
||||
}
|
||||
|
||||
function formatSize(bytes = 0) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// ─── Drop zone ────────────────────────────────────────────────────────────────
|
||||
|
||||
function DropZone({ onFiles }) {
|
||||
@@ -93,7 +88,7 @@ function FileRow({ job, onRetry, onRemove }) {
|
||||
<div className="flex-1 min-w-0 space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium truncate">{job.name}</p>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{formatSize(job.size)}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{formatFileSize(job.size)}</span>
|
||||
</div>
|
||||
<Progress value={job.progress} className={barClass} />
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ArrowLeft, UploadCloud, X, FileVideo, FileText, Image, RefreshCw } from
|
||||
|
||||
import { useAssets } from "@/contexts/AdminAssetsContext";
|
||||
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -46,12 +47,6 @@ function FieldError({ message }) {
|
||||
return <p className="text-xs text-destructive mt-1">{message}</p>;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return "—";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
// ─── Thumbnail Drop Zone ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -249,7 +244,7 @@ export default function EditAsset() {
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" className="text-xs">{asset.file_type}</Badge>
|
||||
<Badge variant="outline" className="text-xs">{asset.extension?.toUpperCase()}</Badge>
|
||||
<span className="text-xs text-muted-foreground">{formatBytes(asset.file_size)}</span>
|
||||
<span className="text-xs text-muted-foreground">{formatFileSize(asset.file_size) ?? "—"}</span>
|
||||
{asset.resolution && (
|
||||
<span className="text-xs text-muted-foreground">{asset.resolution}</span>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// modules/admin/pages/assets/ViewAudioAsset.jsx
|
||||
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft, Lock, Globe, Music2, Download } from "lucide-react";
|
||||
import { ArrowLeft, Lock, Globe, Music2 } from "lucide-react";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
|
||||
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
|
||||
import { downloadAsset } from "@/utils/media.util";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
@@ -71,9 +71,6 @@ export default function ViewAudioAsset() {
|
||||
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
|
||||
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
|
||||
<Download className="h-4 w-4" /> Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
@@ -93,7 +90,7 @@ export default function ViewAudioAsset() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">File Info</p>
|
||||
<MetaRow label="Original Name" value={a.original_name} />
|
||||
<MetaRow label="Extension" value={a.extension} />
|
||||
<MetaRow label="File Size" value={a.file_size ? `${(a.file_size / 1024).toFixed(1)} KB` : null} />
|
||||
<MetaRow label="File Size" value={formatFileSize(a.file_size)} />
|
||||
<MetaRow label="MIME Type" value={a.mime_type} />
|
||||
<Separator className="my-2" />
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Storage</p>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// modules/admin/pages/assets/ViewDocumentAsset.jsx
|
||||
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft, Lock, Globe, FileText, Download } from "lucide-react";
|
||||
import { ArrowLeft, Lock, Globe, FileText } from "lucide-react";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
|
||||
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
|
||||
import { downloadAsset } from "@/utils/media.util";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MediaFallback } from "@/components/generic/MediaFallback";
|
||||
import FileZoomViewer from "@/components/generic/FileZoomViewer";
|
||||
import AssetPageLoader from "@/components/generic/AssetLoader";
|
||||
|
||||
function MetaRow({ label, value }) {
|
||||
@@ -32,7 +31,7 @@ export default function ViewDocumentAsset() {
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
|
||||
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
|
||||
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
|
||||
const { src: streamUrl, loading: previewLoading } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
|
||||
|
||||
if (loading) {
|
||||
return <AssetPageLoader />;
|
||||
@@ -48,7 +47,9 @@ export default function ViewDocumentAsset() {
|
||||
}
|
||||
|
||||
const a = selectedAsset;
|
||||
const canPreview = PREVIEWABLE.includes((a.extension ?? "").toLowerCase());
|
||||
const ext = (a.extension ?? "").toLowerCase();
|
||||
const canPreview = PREVIEWABLE.includes(ext);
|
||||
const isPdf = ext === "pdf";
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 py-6 space-y-6">
|
||||
@@ -62,16 +63,22 @@ export default function ViewDocumentAsset() {
|
||||
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
|
||||
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
|
||||
<Download className="h-4 w-4" /> Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
|
||||
{/* ── Document preview ── */}
|
||||
<div className="lg:col-span-3">
|
||||
{canPreview && streamUrl ? (
|
||||
{isPdf ? (
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<FileZoomViewer
|
||||
src={streamUrl}
|
||||
mimeType={a.mime_type}
|
||||
fileName={a.display_name ?? a.original_name}
|
||||
loading={previewLoading}
|
||||
/>
|
||||
</div>
|
||||
) : canPreview && streamUrl ? (
|
||||
<div className="rounded-lg border overflow-hidden bg-white" style={{ height: 600 }}>
|
||||
<iframe
|
||||
src={streamUrl}
|
||||
@@ -96,7 +103,7 @@ export default function ViewDocumentAsset() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">File Info</p>
|
||||
<MetaRow label="Original Name" value={a.original_name} />
|
||||
<MetaRow label="Extension" value={a.extension} />
|
||||
<MetaRow label="File Size" value={a.file_size ? `${(a.file_size / 1024).toFixed(1)} KB` : null} />
|
||||
<MetaRow label="File Size" value={formatFileSize(a.file_size)} />
|
||||
<MetaRow label="MIME Type" value={a.mime_type} />
|
||||
<Separator className="my-2" />
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Storage</p>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// modules/admin/pages/assets/ViewImageAsset.jsx
|
||||
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
|
||||
import { ArrowLeft, Lock, Globe } from "lucide-react";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
|
||||
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
|
||||
import { downloadAsset } from "@/utils/media.util";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MediaFallback } from "@/components/generic/MediaFallback";
|
||||
import FileZoomViewer from "@/components/generic/FileZoomViewer";
|
||||
import AssetPageLoader from "@/components/generic/AssetLoader";
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ViewImageAsset() {
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
|
||||
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
|
||||
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
|
||||
const { src: streamUrl, loading: previewLoading } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
|
||||
|
||||
if (loading) {
|
||||
return <AssetPageLoader />;
|
||||
@@ -59,26 +59,18 @@ export default function ViewImageAsset() {
|
||||
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
|
||||
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
|
||||
<Download className="h-4 w-4" /> Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
|
||||
{/* ── Image preview ── */}
|
||||
<div className="lg:col-span-3 rounded-lg border bg-muted/30 overflow-hidden flex items-center justify-center min-h-64">
|
||||
{streamUrl ? (
|
||||
<img
|
||||
src={streamUrl}
|
||||
alt={a.display_name ?? a.original_name}
|
||||
className="max-w-full max-h-[520px] object-contain"
|
||||
draggable={false}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
) : (
|
||||
<MediaFallback className="size-full" />
|
||||
)}
|
||||
<div className="lg:col-span-3 rounded-lg border overflow-hidden">
|
||||
<FileZoomViewer
|
||||
src={streamUrl}
|
||||
mimeType={a.mime_type}
|
||||
fileName={a.display_name ?? a.original_name}
|
||||
loading={previewLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Metadata panel ── */}
|
||||
@@ -87,7 +79,7 @@ export default function ViewImageAsset() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">File Info</p>
|
||||
<MetaRow label="Original Name" value={a.original_name} />
|
||||
<MetaRow label="Extension" value={a.extension} />
|
||||
<MetaRow label="File Size" value={a.file_size ? `${(a.file_size / 1024).toFixed(1)} KB` : null} />
|
||||
<MetaRow label="File Size" value={formatFileSize(a.file_size)} />
|
||||
<MetaRow label="Resolution" value={a.resolution} />
|
||||
<MetaRow label="Dimensions" value={a.width && a.height ? `${a.width} × ${a.height}` : null} />
|
||||
<Separator className="my-2" />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// modules/admin/pages/assets/ViewVideoAsset.jsx
|
||||
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
|
||||
import { ArrowLeft, Lock, Globe } from "lucide-react";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
|
||||
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
|
||||
import { downloadAsset } from "@/utils/media.util";
|
||||
import { formatFileSize } from "@/utils/format.util";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MediaFallback } from "@/components/generic/MediaFallback";
|
||||
import { VideoBlock } from "@/components/generic/Blocks/Admin/VideoBlock";
|
||||
import { TranscodeStatusBanner } from "@/components/generic/TranscodeStatusBanner";
|
||||
import AssetPageLoader from "@/components/generic/AssetLoader";
|
||||
|
||||
function MetaRow({ label, value }) {
|
||||
@@ -39,7 +39,6 @@ export default function ViewVideoAsset() {
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
|
||||
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
|
||||
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
|
||||
|
||||
if (loading) {
|
||||
return <AssetPageLoader />;
|
||||
@@ -68,33 +67,25 @@ export default function ViewVideoAsset() {
|
||||
<h1 className="text-xl font-semibold truncate">{a.display_name ?? a.original_name}</h1>
|
||||
<p className="text-sm text-muted-foreground">{a.mime_type}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => downloadAsset(a, { scope: "admin" })}>
|
||||
<Download className="h-4 w-4" /> Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
|
||||
{/* ── Video player ── */}
|
||||
<div className="lg:col-span-3 space-y-3">
|
||||
<div className="rounded-lg border bg-black overflow-hidden aspect-video flex items-center justify-center">
|
||||
{streamUrl ? (
|
||||
<video
|
||||
key={streamUrl}
|
||||
controls
|
||||
controlsList="nodownload"
|
||||
disablePictureInPicture
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
className="w-full h-full"
|
||||
poster={a.thumbnail_url ?? undefined}
|
||||
>
|
||||
<source src={streamUrl} type={a.mime_type ?? "video/mp4"} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : (
|
||||
<MediaFallback className="size-full" />
|
||||
)}
|
||||
</div>
|
||||
<TranscodeStatusBanner status={a.transcode_status} />
|
||||
<VideoBlock
|
||||
readOnly
|
||||
onUpdate={() => {}}
|
||||
content={{
|
||||
asset_id: a.asset_id,
|
||||
storage_provider: a.storage_provider,
|
||||
url: a.file_url,
|
||||
thumbnail_url: a.thumbnail_url,
|
||||
title: a.display_name ?? a.original_name,
|
||||
tag: a.extension?.toUpperCase() ?? "",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Thumbnail strip */}
|
||||
{a.thumbnail_url && (
|
||||
@@ -117,7 +108,7 @@ export default function ViewVideoAsset() {
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Video Info</p>
|
||||
<MetaRow label="Original Name" value={a.original_name} />
|
||||
<MetaRow label="Extension" value={a.extension} />
|
||||
<MetaRow label="File Size" value={a.file_size ? `${(a.file_size / (1024 * 1024)).toFixed(2)} MB` : null} />
|
||||
<MetaRow label="File Size" value={formatFileSize(a.file_size)} />
|
||||
<MetaRow label="Resolution" value={a.resolution} />
|
||||
<MetaRow label="Dimensions" value={a.width && a.height ? `${a.width} × ${a.height}` : null} />
|
||||
<MetaRow label="Duration" value={formatDuration(a.duration)} />
|
||||
|
||||
Reference in New Issue
Block a user