mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
[Untested] Add delete flow for community cards
This commit is contained in:
parent
eec5496954
commit
d05031a906
6 changed files with 155 additions and 0 deletions
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
19
apps/rowboat/app/api/me/route.ts
Normal file
19
apps/rowboat/app/api/me/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "{pendingDeleteItem?.name}" 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue