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:
Trey t
2026-03-23 22:57:57 -05:00
parent cee43980dc
commit b318798ca7
3 changed files with 22 additions and 114 deletions
+1 -9
View File
@@ -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 &amp; 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
View File
@@ -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>
+5 -64
View File
@@ -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 AZ</option> <option value="name-asc">Name A-Z</option>
<option value="name-desc">Name ZA</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>