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}>
|
<Dialog open={helpOpen} onOpenChange={setHelpOpen}>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="sm:max-w-lg max-h-[85vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Asset Library Guide</DialogTitle>
|
<DialogTitle>Asset Library Guide</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -43,14 +43,6 @@ export default function GlobalAssetsPage() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 text-sm">
|
<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>
|
<section>
|
||||||
<h3 className="font-semibold mb-1">Repurpose</h3>
|
<h3 className="font-semibold mb-1">Repurpose</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
|
|||||||
+14
-39
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { RepurposeModal } from "./repurpose-modal";
|
||||||
import { VariationModal } from "./variation-modal";
|
import { VariationModal } from "./variation-modal";
|
||||||
|
|
||||||
@@ -24,16 +24,16 @@ interface Asset {
|
|||||||
|
|
||||||
interface AssetCardProps {
|
interface AssetCardProps {
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
onStatusChange: (id: string, status: string) => void;
|
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onSelect?: (id: string) => void;
|
onSelect?: (id: string) => void;
|
||||||
|
onPushToPostiz?: (assetIds: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssetCard({
|
export function AssetCard({
|
||||||
asset,
|
asset,
|
||||||
onStatusChange,
|
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onPushToPostiz,
|
||||||
}: AssetCardProps) {
|
}: AssetCardProps) {
|
||||||
const [repurposeOpen, setRepurposeOpen] = useState(false);
|
const [repurposeOpen, setRepurposeOpen] = useState(false);
|
||||||
const [variationOpen, setVariationOpen] = useState(false);
|
const [variationOpen, setVariationOpen] = useState(false);
|
||||||
@@ -144,18 +144,6 @@ export function AssetCard({
|
|||||||
{asset.dimensions}
|
{asset.dimensions}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
asset.status === "approved"
|
|
||||||
? "default"
|
|
||||||
: asset.status === "rejected"
|
|
||||||
? "destructive"
|
|
||||||
: "secondary"
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{asset.status}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{metadata.caption && (
|
{metadata.caption && (
|
||||||
@@ -184,33 +172,22 @@ export function AssetCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions — only for images and videos */}
|
{/* Actions */}
|
||||||
{(isImage || isVideo) ? (
|
{(isImage || isVideo) && (
|
||||||
<div className="space-y-2">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<div className="flex gap-2">
|
{onPushToPostiz && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex-1 min-w-0 overflow-hidden text-green-600 hover:text-green-700 hover:bg-green-50"
|
className="flex-1 min-w-0 overflow-hidden"
|
||||||
onClick={() => onStatusChange(asset.id, "approved")}
|
onClick={() => onPushToPostiz([asset.id])}
|
||||||
disabled={asset.status === "approved"}
|
|
||||||
>
|
>
|
||||||
<Check className="h-3 w-3 shrink-0" />
|
<Send className="h-3 w-3 shrink-0" />
|
||||||
<span className="truncate">Approve</span>
|
<span className="truncate">Postiz</span>
|
||||||
</Button>
|
</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 && (
|
{isImage && (
|
||||||
<div className="flex gap-2">
|
<>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -229,11 +206,9 @@ export function AssetCard({
|
|||||||
<Sparkles className="h-3 w-3 shrink-0" />
|
<Sparkles className="h-3 w-3 shrink-0" />
|
||||||
<span className="truncate">Variations</span>
|
<span className="truncate">Variations</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-xs text-muted-foreground text-center">Auto-accepted</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
platform: "all",
|
platform: "all",
|
||||||
type: "all",
|
type: "all",
|
||||||
status: "all",
|
|
||||||
});
|
});
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [sort, setSort] = useState("newest");
|
const [sort, setSort] = useState("newest");
|
||||||
@@ -41,7 +40,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
if (campaignId) params.set("campaignId", campaignId);
|
if (campaignId) params.set("campaignId", campaignId);
|
||||||
if (filters.platform !== "all") params.set("platform", filters.platform);
|
if (filters.platform !== "all") params.set("platform", filters.platform);
|
||||||
if (filters.type !== "all") params.set("type", filters.type);
|
if (filters.type !== "all") params.set("type", filters.type);
|
||||||
if (filters.status !== "all") params.set("status", filters.status);
|
|
||||||
if (search) params.set("search", search);
|
if (search) params.set("search", search);
|
||||||
|
|
||||||
fetch(`/api/assets?${params}`)
|
fetch(`/api/assets?${params}`)
|
||||||
@@ -54,17 +52,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
fetchAssets();
|
fetchAssets();
|
||||||
}, [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) {
|
function toggleSelect(id: string) {
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
const next = new Set(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) => {
|
const sortedAssets = [...assets].sort((a, b) => {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case "oldest":
|
case "oldest":
|
||||||
@@ -139,20 +110,6 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
<option value="script">Scripts</option>
|
<option value="script">Scripts</option>
|
||||||
</select>
|
</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
|
<select
|
||||||
className="h-9 rounded-md border px-3 text-sm"
|
className="h-9 rounded-md border px-3 text-sm"
|
||||||
value={sort}
|
value={sort}
|
||||||
@@ -160,8 +117,8 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
>
|
>
|
||||||
<option value="newest">Newest First</option>
|
<option value="newest">Newest First</option>
|
||||||
<option value="oldest">Oldest First</option>
|
<option value="oldest">Oldest First</option>
|
||||||
<option value="name-asc">Name A–Z</option>
|
<option value="name-asc">Name A-Z</option>
|
||||||
<option value="name-desc">Name Z–A</option>
|
<option value="name-desc">Name Z-A</option>
|
||||||
<option value="platform">Platform</option>
|
<option value="platform">Platform</option>
|
||||||
<option value="type">Type</option>
|
<option value="type">Type</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -173,30 +130,14 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
className="h-9 w-48"
|
className="h-9 w-48"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedIds.size > 0 && (
|
{selectedIds.size > 0 && onPushToPostiz && (
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="ml-auto">
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => bulkUpdateStatus("approved")}
|
|
||||||
>
|
|
||||||
Approve ({selectedIds.size})
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => bulkUpdateStatus("rejected")}
|
|
||||||
>
|
|
||||||
Reject ({selectedIds.size})
|
|
||||||
</Button>
|
|
||||||
{onPushToPostiz && (
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onPushToPostiz(Array.from(selectedIds))}
|
onClick={() => onPushToPostiz(Array.from(selectedIds))}
|
||||||
>
|
>
|
||||||
Push to Postiz ({selectedIds.size})
|
Push to Postiz ({selectedIds.size})
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -212,9 +153,9 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps)
|
|||||||
<AssetCard
|
<AssetCard
|
||||||
key={asset.id}
|
key={asset.id}
|
||||||
asset={asset}
|
asset={asset}
|
||||||
onStatusChange={handleStatusChange}
|
|
||||||
selected={selectedIds.has(asset.id)}
|
selected={selectedIds.has(asset.id)}
|
||||||
onSelect={toggleSelect}
|
onSelect={toggleSelect}
|
||||||
|
onPushToPostiz={onPushToPostiz}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user