mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-12 19:55:19 +02:00
Remove legacy cards imports and fix publish endpoints
This commit is contained in:
parent
19da994ac1
commit
ba78778a2a
5 changed files with 28 additions and 243 deletions
|
|
@ -6,7 +6,7 @@ import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, Down
|
|||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ProgressBar, ProgressStep } from "@/components/ui/progress-bar";
|
||||
import { useUser } from '@auth0/nextjs-auth0';
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface TopBarProps {
|
||||
localProjectName: string;
|
||||
|
|
@ -125,6 +125,16 @@ export function TopBar({
|
|||
}, 2000); // Reset after 2 seconds
|
||||
};
|
||||
|
||||
// After successful community publish, briefly show success and then close modal
|
||||
useEffect(() => {
|
||||
if (communityPublishSuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
onShareModalClose();
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [communityPublishSuccess, onShareModalClose]);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const getUserDisplayName = () => {
|
||||
|
|
@ -827,13 +837,13 @@ export function TopBar({
|
|||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
color={communityPublishSuccess ? "success" : "primary"}
|
||||
onPress={onCommunityPublish}
|
||||
isLoading={communityPublishing}
|
||||
isDisabled={!communityData.name.trim() || !communityData.description.trim() || !communityData.category}
|
||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||
isDisabled={communityPublishSuccess || !communityData.name.trim() || !communityData.description.trim() || !communityData.category}
|
||||
className={`${communityPublishSuccess ? 'bg-green-600 hover:bg-green-700' : 'bg-blue-600 hover:bg-blue-700'} px-6 py-2 text-white font-medium`}
|
||||
>
|
||||
{communityPublishing ? 'Publishing...' : 'Publish to Community'}
|
||||
{communityPublishSuccess ? 'Published' : (communityPublishing ? 'Publishing...' : 'Publish to Community')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
|||
|
|
@ -1653,7 +1653,7 @@ export function WorkflowEditor({
|
|||
|
||||
setCommunityPublishing(true);
|
||||
try {
|
||||
const response = await fetch('/api/community-assistants', {
|
||||
const response = await fetch('/api/assistant-templates', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { Tabs, Tab } from "@/components/ui/tabs";
|
|||
import { Project } from "@/src/entities/models/project";
|
||||
import { z } from "zod";
|
||||
import Link from 'next/link';
|
||||
import { CommunitySection } from '@/components/community/CommunitySection';
|
||||
import { AssistantSection } from '@/components/common/AssistantSection';
|
||||
import { UnifiedTemplatesSection } from '@/components/common/UnifiedTemplatesSection';
|
||||
|
||||
|
|
|
|||
|
|
@ -227,8 +227,8 @@ export function UnifiedTemplatesSection({
|
|||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* Type Filter Pills */}
|
||||
<div className="flex gap-1">
|
||||
{/* Type Filter Segmented Control */}
|
||||
<div className="flex gap-0.5 items-center h-8 rounded-full border border-gray-200 dark:border-gray-700 p-0 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
|
||||
{[
|
||||
{ key: 'all', label: 'All', count: allTemplates.length },
|
||||
{ key: 'prebuilt', label: 'Library', count: prebuiltTemplates.length },
|
||||
|
|
@ -237,10 +237,11 @@ export function UnifiedTemplatesSection({
|
|||
<button
|
||||
key={key}
|
||||
onClick={() => setSelectedType(key as any)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
aria-pressed={selectedType === key}
|
||||
className={`inline-flex items-center h-8 px-2.5 rounded-full text-[13px] font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 ${
|
||||
selectedType === key
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-transparent text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{label} ({count})
|
||||
|
|
@ -253,14 +254,14 @@ export function UnifiedTemplatesSection({
|
|||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="w-32 px-3 py-1.5 pr-8 border border-gray-200 dark:border-gray-700 rounded-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 appearance-none text-sm"
|
||||
className="w-44 h-8 px-4 pr-10 border border-gray-300 dark:border-gray-700 rounded-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 appearance-none text-sm hover:bg-gray-50 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
>
|
||||
<option value="popular">Most Popular</option>
|
||||
<option value="newest">Newest First</option>
|
||||
<option value="alphabetical">A-Z</option>
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="pointer-events-none absolute inset-y-0 right-3 flex items-center">
|
||||
<svg className="w-4 h-4 text-gray-400 -translate-y-[2px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -278,10 +279,10 @@ export function UnifiedTemplatesSection({
|
|||
<button
|
||||
key={category}
|
||||
onClick={() => toggleCategory(category)}
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors border shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 ${
|
||||
selectedCategories.has(category)
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600'
|
||||
? 'bg-blue-50 text-blue-700 border-blue-300 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-700'
|
||||
: 'bg-gray-50 text-gray-700 border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
|
|
|
|||
|
|
@ -1,225 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AssistantSection } from '@/components/common/AssistantSection';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { createProjectFromJsonWithOptions } from '@/app/projects/lib/project-creation-utils';
|
||||
|
||||
interface CommunityAssistant {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
authorId: string;
|
||||
authorName: string;
|
||||
authorEmail?: string | null;
|
||||
isAnonymous: boolean;
|
||||
workflow: any;
|
||||
tags: string[];
|
||||
publishedAt: string;
|
||||
lastUpdatedAt: string;
|
||||
downloadCount: number;
|
||||
likeCount: number;
|
||||
featured: boolean;
|
||||
isPublic: boolean;
|
||||
likes: string[];
|
||||
copilotPrompt?: string;
|
||||
thumbnailUrl?: string | null;
|
||||
}
|
||||
|
||||
interface CommunitySectionProps {
|
||||
onImport?: (assistant: CommunityAssistant) => void;
|
||||
}
|
||||
|
||||
export function CommunitySection({ onImport }: CommunitySectionProps) {
|
||||
const [assistants, setAssistants] = useState<CommunityAssistant[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [importingIds, setImportingIds] = useState<Set<string>>(new Set());
|
||||
const [likedIds, setLikedIds] = useState<Set<string>>(new Set());
|
||||
const router = useRouter();
|
||||
|
||||
// Fetch community assistants
|
||||
const fetchAssistants = async (filters?: { searchQuery: string; selectedCategory: string }) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.searchQuery) params.append('search', filters.searchQuery);
|
||||
if (filters?.selectedCategory) params.append('category', filters.selectedCategory);
|
||||
params.append('source', 'community');
|
||||
const url = `/api/assistant-templates?${params}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to fetch assistants');
|
||||
|
||||
const data = await response.json();
|
||||
setAssistants(data.items || []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching assistants:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load assistants');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load guest likes from session storage
|
||||
const loadGuestLikes = () => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem('guestLikes');
|
||||
if (stored) {
|
||||
const likes = JSON.parse(stored);
|
||||
setLikedIds(new Set(likes));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading guest likes:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Save guest likes to session storage
|
||||
const saveGuestLikes = (likes: Set<string>) => {
|
||||
try {
|
||||
sessionStorage.setItem('guestLikes', JSON.stringify(Array.from(likes)));
|
||||
} catch (err) {
|
||||
console.error('Error saving guest likes:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Get or create consistent guest ID
|
||||
const getGuestId = () => {
|
||||
try {
|
||||
let guestId = sessionStorage.getItem('guestId');
|
||||
if (!guestId) {
|
||||
guestId = `guest-${crypto.randomUUID()}`;
|
||||
sessionStorage.setItem('guestId', guestId);
|
||||
}
|
||||
return guestId;
|
||||
} catch (err) {
|
||||
// Fallback if sessionStorage is not available
|
||||
return `guest-${crypto.randomUUID()}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle like toggle
|
||||
const handleLike = async (item: any) => {
|
||||
const assistant = assistants.find(a => a.id === item.id);
|
||||
if (!assistant) return;
|
||||
|
||||
try {
|
||||
const guestId = getGuestId();
|
||||
const response = await fetch(`/api/assistant-templates/${assistant.id}/like`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-guest-id': guestId,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setLikedIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (data.liked) {
|
||||
newSet.add(assistant.id);
|
||||
} else {
|
||||
newSet.delete(assistant.id);
|
||||
}
|
||||
saveGuestLikes(newSet);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Update the assistant's like count
|
||||
setAssistants(prev => prev.map(a =>
|
||||
a.id === assistant.id
|
||||
? { ...a, likeCount: data.likeCount }
|
||||
: a
|
||||
));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error toggling like:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle share
|
||||
const handleShare = (item: any) => {
|
||||
const assistant = assistants.find(a => a.id === item.id);
|
||||
if (!assistant) return;
|
||||
|
||||
const url = `${window.location.origin}/assistant-templates/${assistant.id}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
// You could add a toast notification here
|
||||
console.log('URL copied to clipboard');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy URL:', err);
|
||||
});
|
||||
};
|
||||
|
||||
// Handle import
|
||||
const handleImport = async (item: any) => {
|
||||
const assistant = assistants.find(a => a.id === item.id);
|
||||
if (!assistant) return;
|
||||
|
||||
if (onImport) {
|
||||
onImport(assistant);
|
||||
return;
|
||||
}
|
||||
|
||||
setImportingIds(prev => new Set(prev).add(assistant.id));
|
||||
try {
|
||||
const response = await fetch(`/api/community-assistants/${assistant.id}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch assistant details');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
await createProjectFromJsonWithOptions({
|
||||
workflowJson: JSON.stringify(data.workflow),
|
||||
router,
|
||||
onSuccess: (projectId) => {
|
||||
router.push(`/projects/${projectId}/workflow`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error creating project:', error);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error importing assistant:', err);
|
||||
// You could add error handling here
|
||||
} finally {
|
||||
setImportingIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(assistant.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
fetchAssistants();
|
||||
loadGuestLikes();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AssistantSection
|
||||
title="Community Assistants"
|
||||
description="Discover and use assistants created by the community."
|
||||
items={assistants.map(assistant => ({
|
||||
id: assistant.id,
|
||||
name: assistant.name,
|
||||
description: assistant.description,
|
||||
category: assistant.category,
|
||||
authorName: assistant.authorName,
|
||||
isAnonymous: assistant.isAnonymous,
|
||||
likeCount: assistant.likeCount,
|
||||
createdAt: assistant.publishedAt,
|
||||
isLiked: likedIds.has(assistant.id)
|
||||
}))}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onItemClick={handleImport}
|
||||
onRetry={() => fetchAssistants()}
|
||||
loadingItemId={Array.from(importingIds)[0] || null}
|
||||
emptyMessage="No community assistants available"
|
||||
onLike={handleLike}
|
||||
onShare={handleShare}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue