mirror of https://github.com/kortix-ai/suna.git
template share UX/UI
This commit is contained in:
parent
5e1e82f67a
commit
43d5245f66
|
@ -157,46 +157,21 @@ const IntegrationIcon: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const Navbar: React.FC<{ template: MarketplaceTemplate | undefined }> = ({ template }) => {
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
|
||||
export default function TemplateSharePage() {
|
||||
const params = useParams();
|
||||
const shareId = params.shareId as string;
|
||||
const router = useRouter();
|
||||
const { scrollY } = useScroll();
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
const { user } = useAuth();
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const [colorPalette, setColorPalette] = useState<string[]>([]);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [isPromptExpanded, setIsPromptExpanded] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = scrollY.on('change', (latest) => {
|
||||
setHasScrolled(latest > 10);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [scrollY]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const sections = ['system-prompt', 'integrations', 'triggers', 'tools'];
|
||||
|
||||
for (const section of sections) {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.top <= 150 && rect.bottom >= 150) {
|
||||
setActiveSection(section);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
handleScroll();
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
// Helper functions and variables for navigation
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
|
@ -206,143 +181,49 @@ const Navbar: React.FC<{ template: MarketplaceTemplate | undefined }> = ({ templ
|
|||
}
|
||||
};
|
||||
|
||||
const logoSrc = !mounted
|
||||
? '/kortix-logo.svg'
|
||||
: resolvedTheme === 'dark'
|
||||
? '/kortix-logo-white.svg'
|
||||
: '/kortix-logo.svg';
|
||||
// Navigation state management
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const sections = ['system-prompt', 'integrations', 'triggers', 'tools'];
|
||||
let currentSection = '';
|
||||
|
||||
// Find the section that's currently in view
|
||||
for (const section of sections) {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
// Use a smaller offset for better responsiveness
|
||||
if (rect.top <= 200 && rect.bottom >= 100) {
|
||||
currentSection = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no section is in the main view area, find the closest one
|
||||
if (!currentSection) {
|
||||
let minDistance = Infinity;
|
||||
for (const section of sections) {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const distance = Math.abs(rect.top - 150);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
currentSection = section;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSection && currentSection !== activeSection) {
|
||||
setActiveSection(currentSection);
|
||||
}
|
||||
};
|
||||
|
||||
const hasIntegrations = template && template.mcp_requirements?.filter((req: any) => req.source === 'tool' && (!req.custom_type || req.custom_type !== 'sse')).length > 0;
|
||||
const hasTriggers = template && template.mcp_requirements?.filter((req: any) => req.source === 'trigger').length > 0;
|
||||
const hasTools = template && template.mcp_requirements?.filter((req: any) => req.source === 'tool' && req.custom_type === 'sse').length > 0;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'sticky z-50 flex justify-center transition-all duration-300',
|
||||
hasScrolled ? 'top-6 mx-4 md:mx-0' : 'top-4 mx-2 md:mx-0',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: hasScrolled ? '1000px' : '100%' }}
|
||||
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="w-full max-w-7xl"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto rounded-2xl transition-all duration-300',
|
||||
hasScrolled
|
||||
? 'px-2 md:px-4 border border-border backdrop-blur-lg bg-background/75'
|
||||
: 'shadow-none px-3 md:px-6',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-14 items-center">
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<KortixLogo size={20} />
|
||||
<span className="font-semibold">Kortix</span>
|
||||
</Link>
|
||||
</div>
|
||||
{template && (
|
||||
<nav className="hidden md:flex items-center space-x-1 ml-8">
|
||||
<button
|
||||
onClick={() => scrollToSection('system-prompt')}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-lg transition-colors",
|
||||
activeSection === 'system-prompt'
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<FileText className="w-4 h-4 inline mr-1.5" />
|
||||
System Prompt
|
||||
</button>
|
||||
{hasIntegrations && (
|
||||
<button
|
||||
onClick={() => scrollToSection('integrations')}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-lg transition-colors",
|
||||
activeSection === 'integrations'
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<Plug className="w-4 h-4 inline mr-1.5" />
|
||||
Integrations
|
||||
</button>
|
||||
)}
|
||||
{hasTriggers && (
|
||||
<button
|
||||
onClick={() => scrollToSection('triggers')}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-lg transition-colors",
|
||||
activeSection === 'triggers'
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<Zap className="w-4 h-4 inline mr-1.5" />
|
||||
Triggers
|
||||
</button>
|
||||
)}
|
||||
{hasTools && (
|
||||
<button
|
||||
onClick={() => scrollToSection('tools')}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-sm rounded-lg transition-colors",
|
||||
activeSection === 'tools'
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<Wrench className="w-4 h-4 inline" />
|
||||
Tools
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
<div className="flex items-center space-x-4 ml-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (template) {
|
||||
router.push(`/agents?tab=marketplace&agent=${template.template_id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
Install Agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default function TemplateSharePage() {
|
||||
const params = useParams();
|
||||
const shareId = params.shareId as string;
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { theme, resolvedTheme } = useTheme();
|
||||
const [colorPalette, setColorPalette] = useState<string[]>([]);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [isPromptExpanded, setIsPromptExpanded] = useState(false);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
handleScroll();
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [activeSection]);
|
||||
|
||||
const { data: template, isLoading, error } = useQuery({
|
||||
queryKey: ['template-share', shareId],
|
||||
|
@ -413,8 +294,7 @@ export default function TemplateSharePage() {
|
|||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar template={undefined} />
|
||||
<div className="flex items-center justify-center h-[calc(100vh-3.5rem)]">
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -424,12 +304,11 @@ export default function TemplateSharePage() {
|
|||
if (error || !template) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar template={undefined} />
|
||||
<div className="flex items-center justify-center h-[calc(100vh-3.5rem)]">
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold mb-2">Template not found</h2>
|
||||
<p className="text-muted-foreground mb-4">The template you're looking for doesn't exist or has been removed.</p>
|
||||
<Button onClick={() => router.push('/agents?tab=marketplace')}>
|
||||
<Button onClick={() => router.push('/agents?tab=marketplace')} className="rounded-lg">
|
||||
Browse Marketplace
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -447,6 +326,11 @@ export default function TemplateSharePage() {
|
|||
.filter(([_, enabled]) => enabled)
|
||||
.map(([toolName]) => toolName);
|
||||
|
||||
// Navigation helper variables
|
||||
const hasIntegrations = integrations.length > 0;
|
||||
const hasTriggers = triggerRequirements.length > 0;
|
||||
const hasTools = customTools.length > 0;
|
||||
|
||||
const getDefaultAvatar = () => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD'];
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
@ -477,20 +361,61 @@ export default function TemplateSharePage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Navbar template={template} />
|
||||
|
||||
<div className="container mx-auto px-4 py-8 mt-8">
|
||||
<Link
|
||||
href="/agents?tab=marketplace"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-8 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Marketplace
|
||||
</Link>
|
||||
{/* Top Navigation Bar */}
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="w-full max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center">
|
||||
<img
|
||||
src={resolvedTheme === 'dark' ? '/kortix-logo-white.svg' : '/kortix-logo.svg'}
|
||||
alt="Kortix"
|
||||
className="h-6"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
className="h-9 w-9 rounded-lg"
|
||||
>
|
||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleShare}
|
||||
className="h-9 w-9 rounded-lg"
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
<span className="sr-only">Share</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Install Agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="w-full max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<div className="lg:col-span-4">
|
||||
<div className="lg:sticky lg:top-32 space-y-6">
|
||||
<div className="lg:sticky lg:top-24 space-y-6">
|
||||
<Link
|
||||
href="/agents?tab=marketplace"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Marketplace
|
||||
</Link>
|
||||
<div className="relative">
|
||||
{colorPalette.length > 0 && (
|
||||
<div
|
||||
|
@ -519,7 +444,7 @@ export default function TemplateSharePage() {
|
|||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{template.name}</h1>
|
||||
{template.is_kortix_team && (
|
||||
<Badge variant="secondary" className="mt-2">
|
||||
<Badge variant="secondary" className="mt-2 bg-primary/10 text-primary">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Official Template
|
||||
</Badge>
|
||||
|
@ -552,22 +477,48 @@ export default function TemplateSharePage() {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={handleInstall}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Content Navigation */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-muted-foreground mb-3">Sections</h3>
|
||||
<nav className="space-y-1">
|
||||
<button
|
||||
onClick={() => scrollToSection('system-prompt')}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg transition-colors text-left flex items-center gap-2 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Install Agent
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleShare}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
<FileText className="w-4 h-4" />
|
||||
System Prompt
|
||||
</button>
|
||||
{hasIntegrations && (
|
||||
<button
|
||||
onClick={() => scrollToSection('integrations')}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg transition-colors text-left flex items-center gap-2 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
>
|
||||
<Plug className="w-4 h-4" />
|
||||
Integrations
|
||||
</button>
|
||||
)}
|
||||
{hasTriggers && (
|
||||
<button
|
||||
onClick={() => scrollToSection('triggers')}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg transition-colors text-left flex items-center gap-2 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Triggers
|
||||
</button>
|
||||
)}
|
||||
{hasTools && (
|
||||
<button
|
||||
onClick={() => scrollToSection('tools')}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg transition-colors text-left flex items-center gap-2 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
>
|
||||
<Wrench className="w-4 h-4" />
|
||||
Tools
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -745,7 +696,7 @@ export default function TemplateSharePage() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card className="bg-muted/30 border-muted/50">
|
||||
{/* <Card className="bg-muted/30 border-muted/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<h3 className="text-2xl font-bold mb-4">Ready to get started?</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
|
@ -768,7 +719,7 @@ export default function TemplateSharePage() {
|
|||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue