import React, { useState, useEffect, useMemo, useRef } from ‘react’;
import {
Search, Plus, Star, ExternalLink, Trash2, Edit3, ChevronLeft,
Youtube, Globe, X, Command, LayoutGrid, Zap, Cpu, Activity
} from ‘lucide-react’;
import { motion, AnimatePresence, useScroll, useSpring } from ‘framer-motion’;
// ─────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────
const CATEGORIES = [‘AI’, ‘DevTools’, ‘Design’, ‘Productivity’, ‘Marketing’, ‘Utilities’, ‘Learning’];
const EMPTY_FORM = { name: ”, url: ”, category: CATEGORIES[0], description: ”, notes: ”, tags: ”, youtube_links: [”], is_favorite: false };
// ← Paste your Make.com webhook URL here
const MAKE_WEBHOOK_URL = ‘https://hook.eu2.make.com/YOUR_WEBHOOK_ID’;
// ─────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────
const generateId = () => Math.random().toString(36).substr(2, 9);
const getYouTubeId = (url) => {
const m = url?.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/);
return (m && m[2].length === 11) ? m[2] : null;
};
// ─────────────────────────────────────────────
// MAKE.COM AUTOFILL (no API keys in frontend)
// ─────────────────────────────────────────────
const autofillViaMake = async (url) => {
const res = await fetch(MAKE_WEBHOOK_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ url }),
});
if (!res.ok) throw new Error(`Webhook error ${res.status}`);
const data = await res.json();
// data must be { name, category, description, tags[] }
return data;
};
// ─────────────────────────────────────────────
// DATA STORE (in-memory)
// ─────────────────────────────────────────────
const useTools = () => {
const [tools, setTools] = useState([
{
id: ‘demo1’, name: ‘Claude AI’, url: ‘https://claude.ai’, category: ‘AI’,
description: “Anthropic’s AI assistant for coding, writing, analysis, and complex reasoning tasks.”,
notes: ‘Best for long-form reasoning. Use for code review and documentation.’,
tags: [‘llm’, ‘anthropic’, ‘chatbot’], youtube_links: [], is_favorite: true,
created_at: new Date().toISOString()
},
{
id: ‘demo2’, name: ‘Figma’, url: ‘https://figma.com’, category: ‘Design’,
description: ‘Collaborative interface design tool for teams. Create, prototype, and hand off designs.’,
notes: ‘Use auto-layout for responsive components. Plugins: Iconify, Unsplash.’,
tags: [‘design’, ‘ui’, ‘prototyping’], youtube_links: [], is_favorite: false,
created_at: new Date(Date.now() – 86400000).toISOString()
},
{
id: ‘demo3’, name: ‘Make’, url: ‘https://make.com’, category: ‘Productivity’,
description: ‘Visual automation platform to connect apps and automate workflows without code.’,
notes: ‘Incredible for webhook automation. Way more flexible than Zapier.’,
tags: [‘automation’, ‘no-code’, ‘workflows’], youtube_links: [], is_favorite: true,
created_at: new Date(Date.now() – 172800000).toISOString()
},
]);
const addTool = (tool) => setTools(prev => [{ …tool, id: generateId(), created_at: new Date().toISOString(), is_favorite: tool.is_favorite || false, tags: tool.tags || [], youtube_links: tool.youtube_links || [] }, …prev]);
const updateTool = (t) => setTools(prev => prev.map(p => p.id === t.id ? t : p));
const deleteTool = (id) => setTools(prev => prev.filter(t => t.id !== id));
const toggleFavorite = (id) => setTools(prev => prev.map(t => t.id === id ? { …t, is_favorite: !t.is_favorite } : t));
return { tools, addTool, updateTool, deleteTool, toggleFavorite };
};
// ─────────────────────────────────────────────
// SHARED VISUAL PRIMITIVES
// ─────────────────────────────────────────────
const AntiGravityOrb = ({ color, size, top, left, delay }) => (
);
const GlassCard = ({ children, onClick, className = ” }) => (
{children}
);
const CategoryPill = ({ label, active, onClick }) => (
{label}
);
const PulsingDot = () => (
);
// ─────────────────────────────────────────────
// TOOL CARD
// ─────────────────────────────────────────────
const ToolCard = ({ tool, onClick, onToggleFavorite }) => (
onClick(tool)} className=”flex flex-col h-full”>
{ e.stopPropagation(); onToggleFavorite(tool.id); }}
className={`p-2 rounded-full transition-all ${tool.is_favorite ? ‘text-[#FF4500] bg-[#FF4500]/10’ : ‘text-white/30 hover:text-[#FF4500] hover:bg-white/5’}`}
>
{/* Name — bright white, clearly readable */}
{tool.name}
{/* Description — lighter grey with enough contrast */}
{tool.description}
{tool.tags?.length > 0 && (
{tool.tags.slice(0, 3).map(t => (
#{t}
))}
)}
{tool.youtube_links?.length > 0 && (
)}
{/* Date — readable warm grey */}
{new Date(tool.created_at).toLocaleDateString()}
);
// ─────────────────────────────────────────────
// TOOL DETAIL PAGE
// ─────────────────────────────────────────────
const ToolDetail = ({ tool, onBack, onEdit, onDelete }) => {
if (!tool) return null;
return (
Back to Tools
{tool.category}
{tool.name}
{tool.description}
{tool.notes && (
)}
{tool.youtube_links?.filter(l => getYouTubeId(l)).length > 0 && (
Videos to Watch
{tool.youtube_links.map((link, idx) => {
const yid = getYouTubeId(link);
return yid ? (
) : null;
})}
)}
{tool.tags?.length > 0 && (
Tags
{tool.tags.map(t => (
#{t}
))}
)}
);
};
// ─────────────────────────────────────────────
// TOOL MODAL — Make.com webhook only, no API keys
// ─────────────────────────────────────────────
const ToolModal = ({ isOpen, onClose, onSave, editingTool }) => {
const [step, setStep] = useState(‘url’); // ‘url’ | ‘form’
const [urlInput, setUrlInput] = useState(”);
const [formData, setFormData] = useState(EMPTY_FORM);
const [aiState, setAiState] = useState(‘idle’); // idle | loading | done | error
const [aiError, setAiError] = useState(”);
const urlRef = useRef(null);
// ── Reset on open / switch between add vs edit ──
useEffect(() => {
if (!isOpen) return;
if (editingTool) {
setFormData({
…editingTool,
tags: editingTool.tags.join(‘, ‘),
youtube_links: editingTool.youtube_links?.length > 0 ? editingTool.youtube_links : [”],
});
setStep(‘form’);
setAiState(‘idle’);
setAiError(”);
} else {
setStep(‘url’);
setUrlInput(”);
setFormData(EMPTY_FORM);
setAiState(‘idle’);
setAiError(”);
}
}, [isOpen, editingTool]);
// Focus URL input when step opens
useEffect(() => {
if (step === ‘url’ && urlRef.current) setTimeout(() => urlRef.current?.focus(), 120);
}, [step]);
// ── Call Make.com webhook ──
const runAutoFill = async (url) => {
setAiState(‘loading’);
setAiError(”);
try {
const result = await autofillViaMake(url);
// Validate & map the response
const name = result.name || ”;
const category = CATEGORIES.includes(result.category) ? result.category : CATEGORIES[0];
const description = result.description || ”;
const tags = Array.isArray(result.tags) ? result.tags.join(‘, ‘) : (typeof result.tags === ‘string’ ? result.tags : ”);
setFormData({ …EMPTY_FORM, url, name, category, description, tags, youtube_links: [”] });
setAiState(‘done’);
setStep(‘form’);
} catch (err) {
console.error(‘Webhook error:’, err);
setAiError(‘The webhook did not respond. Check your Make.com scenario is active, then try again.’);
setAiState(‘error’);
// Still let them continue manually
setFormData(prev => ({ …prev, url }));
}
};
const handleUrlSubmit = () => {
const trimmed = urlInput.trim();
if (!trimmed || aiState === ‘loading’) return;
runAutoFill(trimmed);
};
// Auto-fire on paste if it looks like a URL
const handlePaste = (e) => {
const pasted = e.clipboardData.getData(‘text’).trim();
if (pasted.startsWith(‘http://’) || pasted.startsWith(‘https://’)) {
e.preventDefault();
setUrlInput(pasted);
setTimeout(() => runAutoFill(pasted), 60);
}
};
const handleFormSave = (e) => {
e.preventDefault();
onSave({
…formData,
tags: formData.tags.split(‘,’).map(t => t.trim()).filter(Boolean),
youtube_links: formData.youtube_links.filter(l => l.trim()),
});
};
if (!isOpen) return null;
// ── Shared styles ──
const inp = [
‘w-full px-5 py-4 rounded-2xl text-base font-medium transition-all’,
‘bg-[#F5F3EE] border border-black/10’,
‘text-[#1A1A2E] placeholder:text-[#9AA5B4]’,
‘focus:outline-none focus:ring-4 focus:ring-[#0CABE5]/20 focus:border-[#0CABE5]/40’,
].join(‘ ‘);
const lbl = ‘block text-[10px] font-black uppercase tracking-widest text-[#4A5568] mb-1.5 ml-0.5’;
return (
{/* Close */}
{/* ═══════════════════════════════════════
STEP 1 — URL Entry
═══════════════════════════════════════ */}
{step === ‘url’ && (
{/* Header */}
Add New Tool
Paste a link.
Auto-fill the rest.
Drop any tool URL below. We’ll send it to your Make.com scenario which will research and fill the name, category, description, and tags.
{/* URL input */}
{ setUrlInput(e.target.value); setAiState(‘idle’); setAiError(”); }}
onPaste={handlePaste}
onKeyDown={e => e.key === ‘Enter’ && handleUrlSubmit()}
disabled={aiState === ‘loading’}
className={`${inp} pl-11 border-2 border-dashed border-black/15 focus:border-[#0CABE5]`}
/>
{/* Status banners */}
{aiState === ‘loading’ && (
Sending to Make.com…
Researching name, category, tags & description
)}
{aiState === ‘error’ && (
Webhook failed
{aiError}
)}
{/* Main CTA */}
{aiState === ‘loading’ ? (
<>
Waiting for Make.com…
>
) : (
<> Auto-Fill via Make.com>
)}
{/* Skip */}
)}
{/* ═══════════════════════════════════════
STEP 2 — Full Form
═══════════════════════════════════════ */}
{step === ‘form’ && (
{/* Back button (only when adding, not editing) */}
{!editingTool && (
)}
{/* Header */}
{editingTool ? ‘Editing Entry’ : ‘Review & Save’}
{aiState === ‘done’ && (
✓ Auto-Filled
)}
{editingTool ? ‘Edit Tool’ : (formData.name || ‘New Tool’)}
{aiState === ‘done’ && (
Make.com filled these details — review and adjust anything before saving.
)}
)}
);
};
// ─────────────────────────────────────────────
// STATS BAR
// ─────────────────────────────────────────────
const StatsBar = ({ tools }) => {
const favs = tools.filter(t => t.is_favorite).length;
const cats = […new Set(tools.map(t => t.category))].length;
const withVideos = tools.filter(t => t.youtube_links?.length > 0).length;
return (
{[
{ label: ‘Total Tools’, value: tools.length, color: ‘#0CABE5’ },
{ label: ‘Favorites’, value: favs, color: ‘#FF4500’ },
{ label: ‘Categories’, value: cats, color: ‘#A8C400’ },
{ label: ‘With Videos’, value: withVideos, color: ‘#8899AA’ },
].map(stat => (
{stat.value}
{stat.label}
))}
);
};
// ─────────────────────────────────────────────
// MAIN APP
// ─────────────────────────────────────────────
export default function App() {
const { tools, addTool, updateTool, deleteTool, toggleFavorite } = useTools();
const [view, setView] = useState(‘dashboard’);
const [selectedTool, setSelectedTool] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTool, setEditingTool] = useState(null);
const [searchQuery, setSearchQuery] = useState(”);
const [activeCategory, setActiveCategory] = useState(‘All’);
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001 });
const filteredTools = useMemo(() => tools
.filter(t => {
const s = searchQuery.toLowerCase();
const matchSearch = t.name.toLowerCase().includes(s) || t.description.toLowerCase().includes(s) || t.tags?.some(tag => tag.toLowerCase().includes(s));
const matchCategory = activeCategory === ‘All’ || t.category === activeCategory;
return matchSearch && matchCategory && (!showFavoritesOnly || t.is_favorite);
})
.sort((a, b) => new Date(b.created_at) – new Date(a.created_at)),
[tools, searchQuery, activeCategory, showFavoritesOnly]
);
const openAdd = () => { setEditingTool(null); setIsModalOpen(true); };
const handleSave = (data) => {
if (editingTool) {
updateTool(data);
if (selectedTool?.id === data.id) setSelectedTool(data);
} else {
addTool(data);
}
setIsModalOpen(false);
setEditingTool(null);
};
return (
{/* Scroll progress bar */}
{/* Nav */}
setView(‘dashboard’)}>
GROWWITHJOAN
v3.0 // WEIGHTLESS OPS
Add Tool
{view === ‘dashboard’ ? (
{/* Hero */}
Your Elite Toolkit
Tool Library
{/* Search + filters */}
setSearchQuery(e.target.value)}
className=”w-full pl-20 pr-8 py-6 border border-black/5 rounded-3xl outline-none transition-all text-xl font-light tracking-tight text-[#1A1A2E] placeholder:text-[#9AA5B4]”
style={{ background: ‘rgba(255,255,255,0.88)’, boxShadow: ‘0 8px 30px rgba(0,0,0,0.08)’ }}
onFocus={e => e.target.style.boxShadow = ‘0 0 0 4px rgba(12,171,229,0.15), 0 8px 30px rgba(0,0,0,0.08)’}
onBlur={e => e.target.style.boxShadow = ‘0 8px 30px rgba(0,0,0,0.08)’}
/>
{searchQuery && (
)}
setActiveCategory(‘All’)} />
{CATEGORIES.map(c => setActiveCategory(c)} />)}
setShowFavoritesOnly(!showFavoritesOnly)}
className=”ml-auto p-3.5 rounded-2xl border transition-all”
style={{
background: showFavoritesOnly ? ‘rgba(255,69,0,0.08)’ : ‘white’,
borderColor: showFavoritesOnly ? ‘rgba(255,69,0,0.25)’ : ‘rgba(10,10,10,0.1)’,
color: showFavoritesOnly ? ‘#FF4500’ : ‘#6B7280’,
}}>
{/* Grid */}
{filteredTools.map((tool, index) => (
{ setSelectedTool(t); setView(‘detail’); }} onToggleFavorite={toggleFavorite} />
))}
{filteredTools.length === 0 && (
No tools found
Try a different search or add a new tool.
Add Your First Tool
)}
) : (
{ setView(‘dashboard’); setSelectedTool(null); }}
onEdit={t => { setEditingTool(t); setIsModalOpen(true); }}
onDelete={id => { deleteTool(id); setView(‘dashboard’); setSelectedTool(null); }}
/>
)}
{isModalOpen && (
{ setIsModalOpen(false); setEditingTool(null); }}
onSave={handleSave}
editingTool={editingTool}
/>
)}
);
}
import React, { useState, useEffect, useMemo, useRef } from ‘react’;
import {
Search, Plus, Star, ExternalLink, Trash2, Edit3, ChevronLeft,
Youtube, Globe, X, Command, LayoutGrid, Zap, Cpu, Activity
} from ‘lucide-react’;
import { motion, AnimatePresence, useScroll, useSpring } from ‘framer-motion’;
// ─────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────
const CATEGORIES = [‘AI’, ‘DevTools’, ‘Design’, ‘Productivity’, ‘Marketing’, ‘Utilities’, ‘Learning’];
const EMPTY_FORM = { name: ”, url: ”, category: CATEGORIES[0], description: ”, notes: ”, tags: ”, youtube_links: [”], is_favorite: false };
// ← Paste your Make.com webhook URL here
const MAKE_WEBHOOK_URL = ‘https://hook.eu2.make.com/YOUR_WEBHOOK_ID’;
// ─────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────
const generateId = () => Math.random().toString(36).substr(2, 9);
const getYouTubeId = (url) => {
const m = url?.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/);
return (m && m[2].length === 11) ? m[2] : null;
};
// ─────────────────────────────────────────────
// MAKE.COM AUTOFILL (no API keys in frontend)
// ─────────────────────────────────────────────
const autofillViaMake = async (url) => {
const res = await fetch(MAKE_WEBHOOK_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ url }),
});
if (!res.ok) throw new Error(`Webhook error ${res.status}`);
const data = await res.json();
// data must be { name, category, description, tags[] }
return data;
};
// ─────────────────────────────────────────────
// DATA STORE (in-memory)
// ─────────────────────────────────────────────
const useTools = () => {
const [tools, setTools] = useState([
{
id: ‘demo1’, name: ‘Claude AI’, url: ‘https://claude.ai’, category: ‘AI’,
description: “Anthropic’s AI assistant for coding, writing, analysis, and complex reasoning tasks.”,
notes: ‘Best for long-form reasoning. Use for code review and documentation.’,
tags: [‘llm’, ‘anthropic’, ‘chatbot’], youtube_links: [], is_favorite: true,
created_at: new Date().toISOString()
},
{
id: ‘demo2’, name: ‘Figma’, url: ‘https://figma.com’, category: ‘Design’,
description: ‘Collaborative interface design tool for teams. Create, prototype, and hand off designs.’,
notes: ‘Use auto-layout for responsive components. Plugins: Iconify, Unsplash.’,
tags: [‘design’, ‘ui’, ‘prototyping’], youtube_links: [], is_favorite: false,
created_at: new Date(Date.now() – 86400000).toISOString()
},
{
id: ‘demo3’, name: ‘Make’, url: ‘https://make.com’, category: ‘Productivity’,
description: ‘Visual automation platform to connect apps and automate workflows without code.’,
notes: ‘Incredible for webhook automation. Way more flexible than Zapier.’,
tags: [‘automation’, ‘no-code’, ‘workflows’], youtube_links: [], is_favorite: true,
created_at: new Date(Date.now() – 172800000).toISOString()
},
]);
const addTool = (tool) => setTools(prev => [{ …tool, id: generateId(), created_at: new Date().toISOString(), is_favorite: tool.is_favorite || false, tags: tool.tags || [], youtube_links: tool.youtube_links || [] }, …prev]);
const updateTool = (t) => setTools(prev => prev.map(p => p.id === t.id ? t : p));
const deleteTool = (id) => setTools(prev => prev.filter(t => t.id !== id));
const toggleFavorite = (id) => setTools(prev => prev.map(t => t.id === id ? { …t, is_favorite: !t.is_favorite } : t));
return { tools, addTool, updateTool, deleteTool, toggleFavorite };
};
// ─────────────────────────────────────────────
// SHARED VISUAL PRIMITIVES
// ─────────────────────────────────────────────
const AntiGravityOrb = ({ color, size, top, left, delay }) => (
);
const GlassCard = ({ children, onClick, className = ” }) => (
{children}
);
const CategoryPill = ({ label, active, onClick }) => (
{label}
);
const PulsingDot = () => (
);
// ─────────────────────────────────────────────
// TOOL CARD
// ─────────────────────────────────────────────
const ToolCard = ({ tool, onClick, onToggleFavorite }) => (
onClick(tool)} className=”flex flex-col h-full”>
{ e.stopPropagation(); onToggleFavorite(tool.id); }}
className={`p-2 rounded-full transition-all ${tool.is_favorite ? ‘text-[#FF4500] bg-[#FF4500]/10’ : ‘text-white/30 hover:text-[#FF4500] hover:bg-white/5’}`}
>
{/* Name — bright white, clearly readable */}
{tool.name}
{/* Description — lighter grey with enough contrast */}
{tool.description}
{tool.tags?.length > 0 && (
{tool.tags.slice(0, 3).map(t => (
#{t}
))}
)}
{tool.youtube_links?.length > 0 && (
)}
{/* Date — readable warm grey */}
{new Date(tool.created_at).toLocaleDateString()}
);
// ─────────────────────────────────────────────
// TOOL DETAIL PAGE
// ─────────────────────────────────────────────
const ToolDetail = ({ tool, onBack, onEdit, onDelete }) => {
if (!tool) return null;
return (
Back to Tools
{tool.category}
{tool.name}
{tool.description}
{tool.notes && (
)}
{tool.youtube_links?.filter(l => getYouTubeId(l)).length > 0 && (
Videos to Watch
{tool.youtube_links.map((link, idx) => {
const yid = getYouTubeId(link);
return yid ? (
) : null;
})}
)}
{tool.tags?.length > 0 && (
Tags
{tool.tags.map(t => (
#{t}
))}
)}
);
};
// ─────────────────────────────────────────────
// TOOL MODAL — Make.com webhook only, no API keys
// ─────────────────────────────────────────────
const ToolModal = ({ isOpen, onClose, onSave, editingTool }) => {
const [step, setStep] = useState(‘url’); // ‘url’ | ‘form’
const [urlInput, setUrlInput] = useState(”);
const [formData, setFormData] = useState(EMPTY_FORM);
const [aiState, setAiState] = useState(‘idle’); // idle | loading | done | error
const [aiError, setAiError] = useState(”);
const urlRef = useRef(null);
// ── Reset on open / switch between add vs edit ──
useEffect(() => {
if (!isOpen) return;
if (editingTool) {
setFormData({
…editingTool,
tags: editingTool.tags.join(‘, ‘),
youtube_links: editingTool.youtube_links?.length > 0 ? editingTool.youtube_links : [”],
});
setStep(‘form’);
setAiState(‘idle’);
setAiError(”);
} else {
setStep(‘url’);
setUrlInput(”);
setFormData(EMPTY_FORM);
setAiState(‘idle’);
setAiError(”);
}
}, [isOpen, editingTool]);
// Focus URL input when step opens
useEffect(() => {
if (step === ‘url’ && urlRef.current) setTimeout(() => urlRef.current?.focus(), 120);
}, [step]);
// ── Call Make.com webhook ──
const runAutoFill = async (url) => {
setAiState(‘loading’);
setAiError(”);
try {
const result = await autofillViaMake(url);
// Validate & map the response
const name = result.name || ”;
const category = CATEGORIES.includes(result.category) ? result.category : CATEGORIES[0];
const description = result.description || ”;
const tags = Array.isArray(result.tags) ? result.tags.join(‘, ‘) : (typeof result.tags === ‘string’ ? result.tags : ”);
setFormData({ …EMPTY_FORM, url, name, category, description, tags, youtube_links: [”] });
setAiState(‘done’);
setStep(‘form’);
} catch (err) {
console.error(‘Webhook error:’, err);
setAiError(‘The webhook did not respond. Check your Make.com scenario is active, then try again.’);
setAiState(‘error’);
// Still let them continue manually
setFormData(prev => ({ …prev, url }));
}
};
const handleUrlSubmit = () => {
const trimmed = urlInput.trim();
if (!trimmed || aiState === ‘loading’) return;
runAutoFill(trimmed);
};
// Auto-fire on paste if it looks like a URL
const handlePaste = (e) => {
const pasted = e.clipboardData.getData(‘text’).trim();
if (pasted.startsWith(‘http://’) || pasted.startsWith(‘https://’)) {
e.preventDefault();
setUrlInput(pasted);
setTimeout(() => runAutoFill(pasted), 60);
}
};
const handleFormSave = (e) => {
e.preventDefault();
onSave({
…formData,
tags: formData.tags.split(‘,’).map(t => t.trim()).filter(Boolean),
youtube_links: formData.youtube_links.filter(l => l.trim()),
});
};
if (!isOpen) return null;
// ── Shared styles ──
const inp = [
‘w-full px-5 py-4 rounded-2xl text-base font-medium transition-all’,
‘bg-[#F5F3EE] border border-black/10’,
‘text-[#1A1A2E] placeholder:text-[#9AA5B4]’,
‘focus:outline-none focus:ring-4 focus:ring-[#0CABE5]/20 focus:border-[#0CABE5]/40’,
].join(‘ ‘);
const lbl = ‘block text-[10px] font-black uppercase tracking-widest text-[#4A5568] mb-1.5 ml-0.5’;
return (
{/* Close */}
{/* ═══════════════════════════════════════
STEP 1 — URL Entry
═══════════════════════════════════════ */}
{step === ‘url’ && (
{/* Header */}
Add New Tool
Paste a link.
Auto-fill the rest.
Drop any tool URL below. We’ll send it to your Make.com scenario which will research and fill the name, category, description, and tags.
{/* URL input */}
{ setUrlInput(e.target.value); setAiState(‘idle’); setAiError(”); }}
onPaste={handlePaste}
onKeyDown={e => e.key === ‘Enter’ && handleUrlSubmit()}
disabled={aiState === ‘loading’}
className={`${inp} pl-11 border-2 border-dashed border-black/15 focus:border-[#0CABE5]`}
/>
{/* Status banners */}
{aiState === ‘loading’ && (
Sending to Make.com…
Researching name, category, tags & description
)}
{aiState === ‘error’ && (
Webhook failed
{aiError}
)}
{/* Main CTA */}
{aiState === ‘loading’ ? (
<>
Waiting for Make.com…
>
) : (
<> Auto-Fill via Make.com>
)}
{/* Skip */}
)}
{/* ═══════════════════════════════════════
STEP 2 — Full Form
═══════════════════════════════════════ */}
{step === ‘form’ && (
{/* Back button (only when adding, not editing) */}
{!editingTool && (
)}
{/* Header */}
{editingTool ? ‘Editing Entry’ : ‘Review & Save’}
{aiState === ‘done’ && (
✓ Auto-Filled
)}
{editingTool ? ‘Edit Tool’ : (formData.name || ‘New Tool’)}
{aiState === ‘done’ && (
Make.com filled these details — review and adjust anything before saving.
)}
)}
);
};
// ─────────────────────────────────────────────
// STATS BAR
// ─────────────────────────────────────────────
const StatsBar = ({ tools }) => {
const favs = tools.filter(t => t.is_favorite).length;
const cats = […new Set(tools.map(t => t.category))].length;
const withVideos = tools.filter(t => t.youtube_links?.length > 0).length;
return (
{[
{ label: ‘Total Tools’, value: tools.length, color: ‘#0CABE5’ },
{ label: ‘Favorites’, value: favs, color: ‘#FF4500’ },
{ label: ‘Categories’, value: cats, color: ‘#A8C400’ },
{ label: ‘With Videos’, value: withVideos, color: ‘#8899AA’ },
].map(stat => (
{stat.value}
{stat.label}
))}
);
};
// ─────────────────────────────────────────────
// MAIN APP
// ─────────────────────────────────────────────
export default function App() {
const { tools, addTool, updateTool, deleteTool, toggleFavorite } = useTools();
const [view, setView] = useState(‘dashboard’);
const [selectedTool, setSelectedTool] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTool, setEditingTool] = useState(null);
const [searchQuery, setSearchQuery] = useState(”);
const [activeCategory, setActiveCategory] = useState(‘All’);
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001 });
const filteredTools = useMemo(() => tools
.filter(t => {
const s = searchQuery.toLowerCase();
const matchSearch = t.name.toLowerCase().includes(s) || t.description.toLowerCase().includes(s) || t.tags?.some(tag => tag.toLowerCase().includes(s));
const matchCategory = activeCategory === ‘All’ || t.category === activeCategory;
return matchSearch && matchCategory && (!showFavoritesOnly || t.is_favorite);
})
.sort((a, b) => new Date(b.created_at) – new Date(a.created_at)),
[tools, searchQuery, activeCategory, showFavoritesOnly]
);
const openAdd = () => { setEditingTool(null); setIsModalOpen(true); };
const handleSave = (data) => {
if (editingTool) {
updateTool(data);
if (selectedTool?.id === data.id) setSelectedTool(data);
} else {
addTool(data);
}
setIsModalOpen(false);
setEditingTool(null);
};
return (
{/* Scroll progress bar */}
{/* Nav */}
setView(‘dashboard’)}>
GROWWITHJOAN
v3.0 // WEIGHTLESS OPS
Add Tool
{view === ‘dashboard’ ? (
{/* Hero */}
Your Elite Toolkit
Tool Library
{/* Search + filters */}
setSearchQuery(e.target.value)}
className=”w-full pl-20 pr-8 py-6 border border-black/5 rounded-3xl outline-none transition-all text-xl font-light tracking-tight text-[#1A1A2E] placeholder:text-[#9AA5B4]”
style={{ background: ‘rgba(255,255,255,0.88)’, boxShadow: ‘0 8px 30px rgba(0,0,0,0.08)’ }}
onFocus={e => e.target.style.boxShadow = ‘0 0 0 4px rgba(12,171,229,0.15), 0 8px 30px rgba(0,0,0,0.08)’}
onBlur={e => e.target.style.boxShadow = ‘0 8px 30px rgba(0,0,0,0.08)’}
/>
{searchQuery && (
)}
setActiveCategory(‘All’)} />
{CATEGORIES.map(c => setActiveCategory(c)} />)}
setShowFavoritesOnly(!showFavoritesOnly)}
className=”ml-auto p-3.5 rounded-2xl border transition-all”
style={{
background: showFavoritesOnly ? ‘rgba(255,69,0,0.08)’ : ‘white’,
borderColor: showFavoritesOnly ? ‘rgba(255,69,0,0.25)’ : ‘rgba(10,10,10,0.1)’,
color: showFavoritesOnly ? ‘#FF4500’ : ‘#6B7280’,
}}>
{/* Grid */}
{filteredTools.map((tool, index) => (
{ setSelectedTool(t); setView(‘detail’); }} onToggleFavorite={toggleFavorite} />
))}
{filteredTools.length === 0 && (
No tools found
Try a different search or add a new tool.
Add Your First Tool
)}
) : (
{ setView(‘dashboard’); setSelectedTool(null); }}
onEdit={t => { setEditingTool(t); setIsModalOpen(true); }}
onDelete={id => { deleteTool(id); setView(‘dashboard’); setSelectedTool(null); }}
/>
)}
{isModalOpen && (
{ setIsModalOpen(false); setEditingTool(null); }}
onSave={handleSave}
editingTool={editingTool}
/>
)}
);
}
import React, { useState, useEffect, useMemo, useRef } from ‘react’;
import {
Search, Plus, Star, ExternalLink, Trash2, Edit3, ChevronLeft,
Youtube, Globe, X, Command, LayoutGrid, Zap, Cpu, Activity
} from ‘lucide-react’;
import { motion, AnimatePresence, useScroll, useSpring } from ‘framer-motion’;
// ─────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────
const CATEGORIES = [‘AI’, ‘DevTools’, ‘Design’, ‘Productivity’, ‘Marketing’, ‘Utilities’, ‘Learning’];
const EMPTY_FORM = { name: ”, url: ”, category: CATEGORIES[0], description: ”, notes: ”, tags: ”, youtube_links: [”], is_favorite: false };
// ← Paste your Make.com webhook URL here
const MAKE_WEBHOOK_URL = ‘https://hook.eu2.make.com/YOUR_WEBHOOK_ID’;
// ─────────────────────────────────────────────
// UTILITIES
// ─────────────────────────────────────────────
const generateId = () => Math.random().toString(36).substr(2, 9);
const getYouTubeId = (url) => {
const m = url?.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/);
return (m && m[2].length === 11) ? m[2] : null;
};
// ─────────────────────────────────────────────
// MAKE.COM AUTOFILL (no API keys in frontend)
// ─────────────────────────────────────────────
const autofillViaMake = async (url) => {
const res = await fetch(MAKE_WEBHOOK_URL, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ url }),
});
if (!res.ok) throw new Error(`Webhook error ${res.status}`);
const data = await res.json();
// data must be { name, category, description, tags[] }
return data;
};
// ─────────────────────────────────────────────
// DATA STORE (in-memory)
// ─────────────────────────────────────────────
const useTools = () => {
const [tools, setTools] = useState([
{
id: ‘demo1’, name: ‘Claude AI’, url: ‘https://claude.ai’, category: ‘AI’,
description: “Anthropic’s AI assistant for coding, writing, analysis, and complex reasoning tasks.”,
notes: ‘Best for long-form reasoning. Use for code review and documentation.’,
tags: [‘llm’, ‘anthropic’, ‘chatbot’], youtube_links: [], is_favorite: true,
created_at: new Date().toISOString()
},
{
id: ‘demo2’, name: ‘Figma’, url: ‘https://figma.com’, category: ‘Design’,
description: ‘Collaborative interface design tool for teams. Create, prototype, and hand off designs.’,
notes: ‘Use auto-layout for responsive components. Plugins: Iconify, Unsplash.’,
tags: [‘design’, ‘ui’, ‘prototyping’], youtube_links: [], is_favorite: false,
created_at: new Date(Date.now() – 86400000).toISOString()
},
{
id: ‘demo3’, name: ‘Make’, url: ‘https://make.com’, category: ‘Productivity’,
description: ‘Visual automation platform to connect apps and automate workflows without code.’,
notes: ‘Incredible for webhook automation. Way more flexible than Zapier.’,
tags: [‘automation’, ‘no-code’, ‘workflows’], youtube_links: [], is_favorite: true,
created_at: new Date(Date.now() – 172800000).toISOString()
},
]);
const addTool = (tool) => setTools(prev => [{ …tool, id: generateId(), created_at: new Date().toISOString(), is_favorite: tool.is_favorite || false, tags: tool.tags || [], youtube_links: tool.youtube_links || [] }, …prev]);
const updateTool = (t) => setTools(prev => prev.map(p => p.id === t.id ? t : p));
const deleteTool = (id) => setTools(prev => prev.filter(t => t.id !== id));
const toggleFavorite = (id) => setTools(prev => prev.map(t => t.id === id ? { …t, is_favorite: !t.is_favorite } : t));
return { tools, addTool, updateTool, deleteTool, toggleFavorite };
};
// ─────────────────────────────────────────────
// SHARED VISUAL PRIMITIVES
// ─────────────────────────────────────────────
const AntiGravityOrb = ({ color, size, top, left, delay }) => (
);
const GlassCard = ({ children, onClick, className = ” }) => (
{children}
);
const CategoryPill = ({ label, active, onClick }) => (
{label}
);
const PulsingDot = () => (
);
// ─────────────────────────────────────────────
// TOOL CARD
// ─────────────────────────────────────────────
const ToolCard = ({ tool, onClick, onToggleFavorite }) => (
onClick(tool)} className=”flex flex-col h-full”>
{ e.stopPropagation(); onToggleFavorite(tool.id); }}
className={`p-2 rounded-full transition-all ${tool.is_favorite ? ‘text-[#FF4500] bg-[#FF4500]/10’ : ‘text-white/30 hover:text-[#FF4500] hover:bg-white/5’}`}
>
{/* Name — bright white, clearly readable */}
{tool.name}
{/* Description — lighter grey with enough contrast */}
{tool.description}
{tool.tags?.length > 0 && (
{tool.tags.slice(0, 3).map(t => (
#{t}
))}
)}
{tool.youtube_links?.length > 0 && (
)}
{/* Date — readable warm grey */}
{new Date(tool.created_at).toLocaleDateString()}
);
// ─────────────────────────────────────────────
// TOOL DETAIL PAGE
// ─────────────────────────────────────────────
const ToolDetail = ({ tool, onBack, onEdit, onDelete }) => {
if (!tool) return null;
return (
Back to Tools
{tool.category}
{tool.name}
{tool.description}
{tool.notes && (
)}
{tool.youtube_links?.filter(l => getYouTubeId(l)).length > 0 && (
Videos to Watch
{tool.youtube_links.map((link, idx) => {
const yid = getYouTubeId(link);
return yid ? (
) : null;
})}
)}
{tool.tags?.length > 0 && (
Tags
{tool.tags.map(t => (
#{t}
))}
)}
);
};
// ─────────────────────────────────────────────
// TOOL MODAL — Make.com webhook only, no API keys
// ─────────────────────────────────────────────
const ToolModal = ({ isOpen, onClose, onSave, editingTool }) => {
const [step, setStep] = useState(‘url’); // ‘url’ | ‘form’
const [urlInput, setUrlInput] = useState(”);
const [formData, setFormData] = useState(EMPTY_FORM);
const [aiState, setAiState] = useState(‘idle’); // idle | loading | done | error
const [aiError, setAiError] = useState(”);
const urlRef = useRef(null);
// ── Reset on open / switch between add vs edit ──
useEffect(() => {
if (!isOpen) return;
if (editingTool) {
setFormData({
…editingTool,
tags: editingTool.tags.join(‘, ‘),
youtube_links: editingTool.youtube_links?.length > 0 ? editingTool.youtube_links : [”],
});
setStep(‘form’);
setAiState(‘idle’);
setAiError(”);
} else {
setStep(‘url’);
setUrlInput(”);
setFormData(EMPTY_FORM);
setAiState(‘idle’);
setAiError(”);
}
}, [isOpen, editingTool]);
// Focus URL input when step opens
useEffect(() => {
if (step === ‘url’ && urlRef.current) setTimeout(() => urlRef.current?.focus(), 120);
}, [step]);
// ── Call Make.com webhook ──
const runAutoFill = async (url) => {
setAiState(‘loading’);
setAiError(”);
try {
const result = await autofillViaMake(url);
// Validate & map the response
const name = result.name || ”;
const category = CATEGORIES.includes(result.category) ? result.category : CATEGORIES[0];
const description = result.description || ”;
const tags = Array.isArray(result.tags) ? result.tags.join(‘, ‘) : (typeof result.tags === ‘string’ ? result.tags : ”);
setFormData({ …EMPTY_FORM, url, name, category, description, tags, youtube_links: [”] });
setAiState(‘done’);
setStep(‘form’);
} catch (err) {
console.error(‘Webhook error:’, err);
setAiError(‘The webhook did not respond. Check your Make.com scenario is active, then try again.’);
setAiState(‘error’);
// Still let them continue manually
setFormData(prev => ({ …prev, url }));
}
};
const handleUrlSubmit = () => {
const trimmed = urlInput.trim();
if (!trimmed || aiState === ‘loading’) return;
runAutoFill(trimmed);
};
// Auto-fire on paste if it looks like a URL
const handlePaste = (e) => {
const pasted = e.clipboardData.getData(‘text’).trim();
if (pasted.startsWith(‘http://’) || pasted.startsWith(‘https://’)) {
e.preventDefault();
setUrlInput(pasted);
setTimeout(() => runAutoFill(pasted), 60);
}
};
const handleFormSave = (e) => {
e.preventDefault();
onSave({
…formData,
tags: formData.tags.split(‘,’).map(t => t.trim()).filter(Boolean),
youtube_links: formData.youtube_links.filter(l => l.trim()),
});
};
if (!isOpen) return null;
// ── Shared styles ──
const inp = [
‘w-full px-5 py-4 rounded-2xl text-base font-medium transition-all’,
‘bg-[#F5F3EE] border border-black/10’,
‘text-[#1A1A2E] placeholder:text-[#9AA5B4]’,
‘focus:outline-none focus:ring-4 focus:ring-[#0CABE5]/20 focus:border-[#0CABE5]/40’,
].join(‘ ‘);
const lbl = ‘block text-[10px] font-black uppercase tracking-widest text-[#4A5568] mb-1.5 ml-0.5’;
return (
{/* Close */}
{/* ═══════════════════════════════════════
STEP 1 — URL Entry
═══════════════════════════════════════ */}
{step === ‘url’ && (
{/* Header */}
Add New Tool
Paste a link.
Auto-fill the rest.
Drop any tool URL below. We’ll send it to your Make.com scenario which will research and fill the name, category, description, and tags.
{/* URL input */}
{ setUrlInput(e.target.value); setAiState(‘idle’); setAiError(”); }}
onPaste={handlePaste}
onKeyDown={e => e.key === ‘Enter’ && handleUrlSubmit()}
disabled={aiState === ‘loading’}
className={`${inp} pl-11 border-2 border-dashed border-black/15 focus:border-[#0CABE5]`}
/>
{/* Status banners */}
{aiState === ‘loading’ && (
Sending to Make.com…
Researching name, category, tags & description
)}
{aiState === ‘error’ && (
Webhook failed
{aiError}
)}
{/* Main CTA */}
{aiState === ‘loading’ ? (
<>
Waiting for Make.com…
>
) : (
<> Auto-Fill via Make.com>
)}
{/* Skip */}
)}
{/* ═══════════════════════════════════════
STEP 2 — Full Form
═══════════════════════════════════════ */}
{step === ‘form’ && (
{/* Back button (only when adding, not editing) */}
{!editingTool && (
)}
{/* Header */}
{editingTool ? ‘Editing Entry’ : ‘Review & Save’}
{aiState === ‘done’ && (
✓ Auto-Filled
)}
{editingTool ? ‘Edit Tool’ : (formData.name || ‘New Tool’)}
{aiState === ‘done’ && (
Make.com filled these details — review and adjust anything before saving.
)}
)}
);
};
// ─────────────────────────────────────────────
// STATS BAR
// ─────────────────────────────────────────────
const StatsBar = ({ tools }) => {
const favs = tools.filter(t => t.is_favorite).length;
const cats = […new Set(tools.map(t => t.category))].length;
const withVideos = tools.filter(t => t.youtube_links?.length > 0).length;
return (
{[
{ label: ‘Total Tools’, value: tools.length, color: ‘#0CABE5’ },
{ label: ‘Favorites’, value: favs, color: ‘#FF4500’ },
{ label: ‘Categories’, value: cats, color: ‘#A8C400’ },
{ label: ‘With Videos’, value: withVideos, color: ‘#8899AA’ },
].map(stat => (
{stat.value}
{stat.label}
))}
);
};
// ─────────────────────────────────────────────
// MAIN APP
// ─────────────────────────────────────────────
export default function App() {
const { tools, addTool, updateTool, deleteTool, toggleFavorite } = useTools();
const [view, setView] = useState(‘dashboard’);
const [selectedTool, setSelectedTool] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTool, setEditingTool] = useState(null);
const [searchQuery, setSearchQuery] = useState(”);
const [activeCategory, setActiveCategory] = useState(‘All’);
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001 });
const filteredTools = useMemo(() => tools
.filter(t => {
const s = searchQuery.toLowerCase();
const matchSearch = t.name.toLowerCase().includes(s) || t.description.toLowerCase().includes(s) || t.tags?.some(tag => tag.toLowerCase().includes(s));
const matchCategory = activeCategory === ‘All’ || t.category === activeCategory;
return matchSearch && matchCategory && (!showFavoritesOnly || t.is_favorite);
})
.sort((a, b) => new Date(b.created_at) – new Date(a.created_at)),
[tools, searchQuery, activeCategory, showFavoritesOnly]
);
const openAdd = () => { setEditingTool(null); setIsModalOpen(true); };
const handleSave = (data) => {
if (editingTool) {
updateTool(data);
if (selectedTool?.id === data.id) setSelectedTool(data);
} else {
addTool(data);
}
setIsModalOpen(false);
setEditingTool(null);
};
return (
{/* Scroll progress bar */}
{/* Nav */}
setView(‘dashboard’)}>
GROWWITHJOAN
v3.0 // WEIGHTLESS OPS
Add Tool
{view === ‘dashboard’ ? (
{/* Hero */}
Your Elite Toolkit
Tool Library
{/* Search + filters */}
setSearchQuery(e.target.value)}
className=”w-full pl-20 pr-8 py-6 border border-black/5 rounded-3xl outline-none transition-all text-xl font-light tracking-tight text-[#1A1A2E] placeholder:text-[#9AA5B4]”
style={{ background: ‘rgba(255,255,255,0.88)’, boxShadow: ‘0 8px 30px rgba(0,0,0,0.08)’ }}
onFocus={e => e.target.style.boxShadow = ‘0 0 0 4px rgba(12,171,229,0.15), 0 8px 30px rgba(0,0,0,0.08)’}
onBlur={e => e.target.style.boxShadow = ‘0 8px 30px rgba(0,0,0,0.08)’}
/>
{searchQuery && (
)}
setActiveCategory(‘All’)} />
{CATEGORIES.map(c => setActiveCategory(c)} />)}
setShowFavoritesOnly(!showFavoritesOnly)}
className=”ml-auto p-3.5 rounded-2xl border transition-all”
style={{
background: showFavoritesOnly ? ‘rgba(255,69,0,0.08)’ : ‘white’,
borderColor: showFavoritesOnly ? ‘rgba(255,69,0,0.25)’ : ‘rgba(10,10,10,0.1)’,
color: showFavoritesOnly ? ‘#FF4500’ : ‘#6B7280’,
}}>
{/* Grid */}
{filteredTools.map((tool, index) => (
{ setSelectedTool(t); setView(‘detail’); }} onToggleFavorite={toggleFavorite} />
))}
{filteredTools.length === 0 && (
No tools found
Try a different search or add a new tool.
Add Your First Tool
)}
) : (
{ setView(‘dashboard’); setSelectedTool(null); }}
onEdit={t => { setEditingTool(t); setIsModalOpen(true); }}
onDelete={id => { deleteTool(id); setView(‘dashboard’); setSelectedTool(null); }}
/>
)}
{isModalOpen && (
{ setIsModalOpen(false); setEditingTool(null); }}
onSave={handleSave}
editingTool={editingTool}
/>
)}
);
}