[Untested] Add delete flow for community cards

This commit is contained in:
akhisud3195 2025-09-15 14:51:41 +04:00
parent eec5496954
commit d05031a906
6 changed files with 155 additions and 0 deletions

View file

@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { MongoDBAssistantTemplatesRepository } from '@/src/infrastructure/repositories/mongodb.assistant-templates.repository';
import { authCheck } from '@/app/actions/auth.actions';
import { USE_AUTH } from '@/app/lib/feature_flags';
const repo = new MongoDBAssistantTemplatesRepository();
@ -15,4 +17,41 @@ export async function GET(_req: NextRequest, context: { params: Promise<{ id: st
}
}
export async function DELETE(_req: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const { id } = await context.params;
const item = await repo.fetch(id);
if (!item) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// Disallow deleting library/prebuilt items
if ((item as any).source === 'library' || item.authorId === 'rowboat-system') {
return NextResponse.json({ error: 'Not allowed' }, { status: 403 });
}
let user;
if (USE_AUTH) {
user = await authCheck();
} else {
user = { id: 'guest_user' } as any; // guest mode acts as a single user
}
if (item.authorId !== user.id) {
// Do not reveal existence
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const ok = await repo.deleteByIdAndAuthor(id, user.id);
if (!ok) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting assistant template:', error);
return NextResponse.json({ error: 'Failed to delete assistant template' }, { status: 500 });
}
}

View file

@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
import { authCheck } from '@/app/actions/auth.actions';
import { USE_AUTH } from '@/app/lib/feature_flags';
export async function GET(_req: NextRequest) {
try {
let user;
if (USE_AUTH) {
user = await authCheck();
} else {
user = { id: 'guest_user' } as any;
}
return NextResponse.json({ id: user.id });
} catch (error) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
}

View file

@ -608,6 +608,8 @@ export function BuildAssistantSection() {
name: template.name,
description: template.description,
category: template.category,
authorId: template.authorId,
source: template.source,
authorName: template.authorName,
isAnonymous: template.isAnonymous,
likeCount: template.likeCount,
@ -625,6 +627,19 @@ export function BuildAssistantSection() {
loadingItemId={loadingTemplateId}
onLike={handleTemplateLike}
onShare={handleTemplateShare}
onDelete={async (item) => {
if (!confirm('Delete this template? This action cannot be undone.')) return;
try {
const resp = await fetch(`/api/assistant-templates/${item.id}`, { method: 'DELETE' });
if (!resp.ok) {
throw new Error('Failed to delete template');
}
setCommunityTemplates(prev => prev.filter(t => t.id !== item.id));
} catch (e) {
console.error(e);
alert('Failed to delete template');
}
}}
getUniqueTools={getUniqueTools}
/>
</div>

View file

@ -60,6 +60,7 @@ interface AssistantCardProps {
createdAt?: string;
onLike?: () => void;
onShare?: () => void;
onDelete?: () => void;
isLiked?: boolean;
// Template type indicator
templateType?: 'prebuilt' | 'community';
@ -83,6 +84,7 @@ export function AssistantCard({
onLike,
onShare,
isLiked = false,
onDelete,
templateType,
onClick,
loading = false,
@ -268,6 +270,20 @@ export function AssistantCard({
<Share2 size={14} className={copied ? "text-blue-600" : undefined} />
{copied && <span className="text-[10px] text-blue-600">Copied</span>}
</button>
{onDelete && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete();
}}
className="flex items-center gap-1 hover:text-red-600 transition-colors"
aria-label="Delete template"
>
{/* small x icon */}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>
</button>
)}
</div>
</div>

View file

@ -11,6 +11,8 @@ interface TemplateItem {
name: string;
description: string;
category: string;
authorId?: string;
source?: 'library' | 'community';
tools?: Array<{
name: string;
logo?: string;
@ -35,6 +37,7 @@ interface UnifiedTemplatesSectionProps {
loadingItemId?: string | null;
onLike?: (item: TemplateItem) => void;
onShare?: (item: TemplateItem) => void;
onDelete?: (item: TemplateItem) => void;
getUniqueTools?: (item: TemplateItem) => Array<{ name: string; logo?: string }>;
}
@ -48,12 +51,29 @@ export function UnifiedTemplatesSection({
loadingItemId = null,
onLike,
onShare,
onDelete,
getUniqueTools
}: UnifiedTemplatesSectionProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedType, setSelectedType] = useState<'all' | 'prebuilt' | 'community'>('all');
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
const [sortBy, setSortBy] = useState<'popular' | 'newest' | 'alphabetical'>('popular');
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingDeleteItem, setPendingDeleteItem] = useState<TemplateItem | null>(null);
useEffect(() => {
let isMounted = true;
(async () => {
try {
const resp = await fetch('/api/me', { cache: 'no-store' });
if (!resp.ok) return;
const data = await resp.json();
if (isMounted) setCurrentUserId(data.id || null);
} catch (_e) {}
})();
return () => { isMounted = false; };
}, []);
// Combine all templates
const allTemplates = useMemo(() => {
@ -345,6 +365,10 @@ export function UnifiedTemplatesSection({
getUniqueTools={getUniqueTools}
onLike={() => onLike?.(item)}
onShare={() => onShare?.(item)}
onDelete={onDelete && currentUserId && item.type === 'community' && item.authorId === currentUserId ? () => {
setPendingDeleteItem(item);
setConfirmOpen(true);
} : undefined}
isLiked={item.isLiked}
templateType={item.type}
/>
@ -353,6 +377,38 @@ export function UnifiedTemplatesSection({
</>
)}
</div>
{/* Delete confirmation modal */}
{confirmOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 max-w-sm w-full p-5">
<div className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Delete template?</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
This will permanently remove &quot;{pendingDeleteItem?.name}&quot; from the community templates. This action cannot be undone.
</div>
<div className="mt-5 flex justify-end gap-2">
<button
onClick={() => { setConfirmOpen(false); setPendingDeleteItem(null); }}
className="px-4 py-2 text-sm rounded-md bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
Cancel
</button>
<button
onClick={async () => {
if (pendingDeleteItem && onDelete) {
await onDelete(pendingDeleteItem);
}
setConfirmOpen(false);
setPendingDeleteItem(null);
}}
className="px-4 py-2 text-sm rounded-md bg-red-600 hover:bg-red-700 text-white"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -77,6 +77,16 @@ export class MongoDBAssistantTemplatesRepository {
const categories = await this.collection.distinct('category', { isPublic: true });
return categories.filter(Boolean);
}
async deleteByIdAndAuthor(id: string, authorId: string): Promise<boolean> {
const result = await this.collection.deleteOne({ _id: new ObjectId(id), authorId } as any);
if (result.deletedCount && result.deletedCount > 0) {
// Clean up likes associated with this assistant template
await this.likesCollection.deleteMany({ assistantId: id } as any);
return true;
}
return false;
}
}