refactor: remove approve/reject, add per-asset Postiz push button
- Remove approve/reject buttons and status badge from asset cards - Remove bulk approve/reject from gallery toolbar - Remove status filter dropdown (no longer relevant) - Add "Postiz" button on each image/video asset card for direct publishing - Keep bulk "Push to Postiz" for multi-select in toolbar - Update How This Works guide to remove approve/reject section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ export default function GlobalAssetsPage() {
|
||||
)}
|
||||
|
||||
<Dialog open={helpOpen} onOpenChange={setHelpOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent className="sm:max-w-lg max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Asset Library Guide</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -43,14 +43,6 @@ export default function GlobalAssetsPage() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 text-sm">
|
||||
<section>
|
||||
<h3 className="font-semibold mb-1">Review & Approve</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Every asset starts as a draft. Use <strong>Approve</strong> or <strong>Reject</strong> to
|
||||
triage. Select multiple assets with checkboxes for bulk actions.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="font-semibold mb-1">Repurpose</h3>
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
+14
-39
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, X, Play, Copy, Sparkles } from "lucide-react";
|
||||
import { Play, Copy, Sparkles, Send } from "lucide-react";
|
||||
import { RepurposeModal } from "./repurpose-modal";
|
||||
import { VariationModal } from "./variation-modal";
|
||||
|
||||
@@ -24,16 +24,16 @@ interface Asset {
|
||||
|
||||
interface AssetCardProps {
|
||||
asset: Asset;
|
||||
onStatusChange: (id: string, status: string) => void;
|
||||
selected?: boolean;
|
||||
onSelect?: (id: string) => void;
|
||||
onPushToPostiz?: (assetIds: string[]) => void;
|
||||
}
|
||||
|
||||
export function AssetCard({
|
||||
asset,
|
||||
onStatusChange,
|
||||
selected,
|
||||
onSelect,
|
||||
onPushToPostiz,
|
||||
}: AssetCardProps) {
|
||||
const [repurposeOpen, setRepurposeOpen] = useState(false);
|
||||
const [variationOpen, setVariationOpen] = useState(false);
|
||||
@@ -144,18 +144,6 @@ export function AssetCard({
|
||||
{asset.dimensions}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant={
|
||||
asset.status === "approved"
|
||||
? "default"
|
||||
: asset.status === "rejected"
|
||||
? "destructive"
|
||||
: "secondary"
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{asset.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{metadata.caption && (
|
||||
@@ -184,33 +172,22 @@ export function AssetCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions — only for images and videos */}
|
||||
{(isImage || isVideo) ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
{/* Actions */}
|
||||
{(isImage || isVideo) && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{onPushToPostiz && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 min-w-0 overflow-hidden text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
onClick={() => onStatusChange(asset.id, "approved")}
|
||||
disabled={asset.status === "approved"}
|
||||
className="flex-1 min-w-0 overflow-hidden"
|
||||
onClick={() => onPushToPostiz([asset.id])}
|
||||
>
|
||||
<Check className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">Approve</span>
|
||||
<Send className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">Postiz</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1 min-w-0 overflow-hidden text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => onStatusChange(asset.id, "rejected")}
|
||||
disabled={asset.status === "rejected"}
|
||||
>
|
||||
<X className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">Reject</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isImage && (
|
||||
<div className="flex gap-2">
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -229,11 +206,9 @@ export function AssetCard({
|
||||
<Sparkles className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">Variations</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground text-center">Auto-accepted</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
const [filters, setFilters] = useState({
|
||||
platform: "all",
|
||||
type: "all",
|
||||
status: "all",
|
||||
});
|
||||
const [search, setSearch] = useState("");
|
||||
const [sort, setSort] = useState("newest");
|
||||
@@ -41,7 +40,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
if (campaignId) params.set("campaignId", campaignId);
|
||||
if (filters.platform !== "all") params.set("platform", filters.platform);
|
||||
if (filters.type !== "all") params.set("type", filters.type);
|
||||
if (filters.status !== "all") params.set("status", filters.status);
|
||||
if (search) params.set("search", search);
|
||||
|
||||
fetch(`/api/assets?${params}`)
|
||||
@@ -54,17 +52,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
fetchAssets();
|
||||
}, [fetchAssets]);
|
||||
|
||||
async function handleStatusChange(id: string, status: string) {
|
||||
await fetch(`/api/assets/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
setAssets((prev) =>
|
||||
prev.map((a) => (a.id === id ? { ...a, status } : a))
|
||||
);
|
||||
}
|
||||
|
||||
function toggleSelect(id: string) {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -74,22 +61,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
});
|
||||
}
|
||||
|
||||
async function bulkUpdateStatus(status: string) {
|
||||
await Promise.all(
|
||||
Array.from(selectedIds).map((id) =>
|
||||
fetch(`/api/assets/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
)
|
||||
);
|
||||
setAssets((prev) =>
|
||||
prev.map((a) => (selectedIds.has(a.id) ? { ...a, status } : a))
|
||||
);
|
||||
setSelectedIds(new Set());
|
||||
}
|
||||
|
||||
const sortedAssets = [...assets].sort((a, b) => {
|
||||
switch (sort) {
|
||||
case "oldest":
|
||||
@@ -139,20 +110,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
<option value="script">Scripts</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="h-9 rounded-md border px-3 text-sm"
|
||||
value={filters.status}
|
||||
onChange={(e) =>
|
||||
setFilters((f) => ({ ...f, status: e.target.value }))
|
||||
}
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="published">Published</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="h-9 rounded-md border px-3 text-sm"
|
||||
value={sort}
|
||||
@@ -160,8 +117,8 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
>
|
||||
<option value="newest">Newest First</option>
|
||||
<option value="oldest">Oldest First</option>
|
||||
<option value="name-asc">Name A–Z</option>
|
||||
<option value="name-desc">Name Z–A</option>
|
||||
<option value="name-asc">Name A-Z</option>
|
||||
<option value="name-desc">Name Z-A</option>
|
||||
<option value="platform">Platform</option>
|
||||
<option value="type">Type</option>
|
||||
</select>
|
||||
@@ -173,30 +130,14 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
className="h-9 w-48"
|
||||
/>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex gap-2 ml-auto">
|
||||
{selectedIds.size > 0 && onPushToPostiz && (
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => bulkUpdateStatus("approved")}
|
||||
onClick={() => onPushToPostiz(Array.from(selectedIds))}
|
||||
>
|
||||
Approve ({selectedIds.size})
|
||||
Push to Postiz ({selectedIds.size})
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => bulkUpdateStatus("rejected")}
|
||||
>
|
||||
Reject ({selectedIds.size})
|
||||
</Button>
|
||||
{onPushToPostiz && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onPushToPostiz(Array.from(selectedIds))}
|
||||
>
|
||||
Push to Postiz ({selectedIds.size})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -212,9 +153,9 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
||||
<AssetCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
onStatusChange={handleStatusChange}
|
||||
selected={selectedIds.has(asset.id)}
|
||||
onSelect={toggleSelect}
|
||||
onPushToPostiz={onPushToPostiz}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user