HTML Academy

Learn HTML from scratch

Lesson 1 of 6

Introduction to HTML

Learn what HTML is and why it's the foundation of every website.

HTML stands for HyperText Markup Language. It's the standard language for creating web pages. Every website you visit is built with HTML at its core.

Code Example

HTML
<!DOCTYPE html>
<html>
  <head>
    <title>My First Page</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>
1 / 6
🔍 Developer Tools
JustCopy.ai Clone with JustCopy
\n`\n },\n {\n id: 2,\n title: \"HTML Elements & Tags\",\n icon: \"Code\",\n description: \"Understand how HTML tags work to structure your content.\",\n content: `HTML uses tags to define elements. Tags usually come in pairs: an opening tag and a closing tag. The content goes between them.`,\n code: `Content goes here\n\n\n

This is a heading

\n

This is a paragraph

\nThis is bold text`\n },\n {\n id: 3,\n title: \"Headings & Paragraphs\",\n icon: \"Type\",\n description: \"Master text formatting with headings and paragraphs.\",\n content: `HTML has 6 heading levels (h1-h6) and paragraphs (p). Headings help organize content and improve SEO.`,\n code: `

Main Title (Largest)

\n

Section Title

\n

Subsection

\n\n

This is a paragraph of text. \n Paragraphs automatically add \n spacing above and below.

`\n },\n {\n id: 4,\n title: \"Links & Navigation\",\n icon: \"Link\",\n description: \"Create clickable links to connect pages together.\",\n content: `The anchor tag creates hyperlinks. The href attribute specifies where the link goes. Links are what make the web interconnected!`,\n code: `\n\n Visit Google\n\n\n\nAbout Us\n\n\n\n Open in New Tab\n`\n },\n {\n id: 5,\n title: \"Images & Media\",\n icon: \"Image\",\n description: \"Add images and visual content to your pages.\",\n content: `The tag embeds images. It's a self-closing tag that requires src (source) and alt (alternative text) attributes.`,\n code: `\"A\n\n\n\n \"Click\n`\n },\n {\n id: 6,\n title: \"Lists\",\n icon: \"List\",\n description: \"Organize content with ordered and unordered lists.\",\n content: `HTML offers two main list types: unordered lists (ul) with bullets, and ordered lists (ol) with numbers. Each item uses the li tag.`,\n code: `\n\n\n\n
    \n
  1. First step
  2. \n
  3. Second step
  4. \n
  5. Third step
  6. \n
`\n }\n]\n\nfunction LessonCard({ lesson, isActive, onClick }) {\n const IconComponent = icons[lesson.icon] || icons.FileCode\n \n return (\n React.createElement('button', {\n onClick: onClick,\n className: `w-full text-left p-4 rounded-xl transition-all duration-300 ${\n isActive \n ? 'bg-gradient-to-r from-orange-500 to-pink-500 text-white shadow-lg scale-[1.02]' \n : 'bg-white hover:bg-gray-50 border border-gray-200 hover:border-orange-300'\n }`,}\n\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: `p-2 rounded-lg ${isActive ? 'bg-white/20' : 'bg-orange-100'}`,}\n , React.createElement(IconComponent, { className: `w-5 h-5 ${isActive ? 'text-white' : 'text-orange-600'}`,} )\n )\n , React.createElement('div', null\n , React.createElement('h3', { className: `font-semibold ${isActive ? 'text-white' : 'text-gray-800'}`,}\n , lesson.title\n )\n , React.createElement('p', { className: `text-sm ${isActive ? 'text-white/80' : 'text-gray-500'}`,}, \"Lesson \"\n , lesson.id\n )\n )\n )\n )\n )\n}\n\nfunction CodeBlock({ code }) {\n const [copied, setCopied] = useState(false)\n \n const handleCopy = () => {\n navigator.clipboard.writeText(code)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n }\n \n return (\n React.createElement('div', { className: \"relative bg-gray-900 rounded-xl overflow-hidden\" ,}\n , React.createElement('div', { className: \"flex items-center justify-between px-4 py-2 bg-gray-800\" ,}\n , React.createElement('span', { className: \"text-gray-400 text-sm font-mono\" ,}, \"HTML\")\n , React.createElement('button', { \n onClick: handleCopy,\n className: \"flex items-center gap-1 text-gray-400 hover:text-white transition-colors text-sm\" ,}\n\n , copied ? React.createElement(icons.Check, { className: \"w-4 h-4\" ,} ) : React.createElement(icons.Copy, { className: \"w-4 h-4\" ,} )\n , copied ? 'Copied!' : 'Copy'\n )\n )\n , React.createElement('pre', { className: \"p-4 overflow-x-auto\" ,}\n , React.createElement('code', { className: \"text-green-400 text-sm font-mono\" ,}, code)\n )\n )\n )\n}\n\n function HTMLLessons() {\n const [activeLesson, setActiveLesson] = useState(lessons[0])\n const [showMobileMenu, setShowMobileMenu] = useState(false)\n \n return (\n React.createElement('div', { className: \"min-h-screen bg-gradient-to-br from-gray-50 to-orange-50\" ,}\n /* Header */\n , React.createElement('header', { className: \"bg-white border-b border-gray-200 sticky top-0 z-50\" ,}\n , React.createElement('div', { className: \"max-w-7xl mx-auto px-4 py-4 flex items-center justify-between\" ,}\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: \"p-2 bg-gradient-to-r from-orange-500 to-pink-500 rounded-xl\" ,}\n , React.createElement(icons.Code, { className: \"w-6 h-6 text-white\" ,} )\n )\n , React.createElement('div', null\n , React.createElement('h1', { className: \"text-xl font-bold text-gray-800\" ,}, \"HTML Academy\" )\n , React.createElement('p', { className: \"text-xs text-gray-500\" ,}, \"Learn HTML from scratch\" )\n )\n )\n , React.createElement('button', { \n onClick: () => setShowMobileMenu(!showMobileMenu),\n className: \"lg:hidden p-2 hover:bg-gray-100 rounded-lg\" ,}\n\n , React.createElement(icons.Menu, { className: \"w-6 h-6 text-gray-600\" ,} )\n )\n )\n )\n\n , React.createElement('div', { className: \"max-w-7xl mx-auto px-4 py-8\" ,}\n , React.createElement('div', { className: \"flex flex-col lg:flex-row gap-8\" ,}\n /* Sidebar - Lesson List */\n , React.createElement('aside', { className: `lg:w-80 flex-shrink-0 ${showMobileMenu ? 'block' : 'hidden lg:block'}`,}\n , React.createElement('div', { className: \"bg-white rounded-2xl p-4 shadow-sm border border-gray-100\" ,}\n , React.createElement('h2', { className: \"text-lg font-bold text-gray-800 mb-4 flex items-center gap-2\" ,}\n , React.createElement(icons.BookOpen, { className: \"w-5 h-5 text-orange-500\" ,} ), \"Lessons\"\n\n )\n , React.createElement('div', { className: \"space-y-3\",}\n , lessons.map(lesson => (\n React.createElement(LessonCard, { \n key: lesson.id,\n lesson: lesson,\n isActive: activeLesson.id === lesson.id,\n onClick: () => {\n setActiveLesson(lesson)\n setShowMobileMenu(false)\n },}\n )\n ))\n )\n )\n )\n\n /* Main Content */\n , React.createElement('main', { className: \"flex-1\",}\n , React.createElement('div', { className: \"bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden\" ,}\n /* Lesson Header */\n , React.createElement('div', { className: \"bg-gradient-to-r from-orange-500 to-pink-500 p-8 text-white\" ,}\n , React.createElement('div', { className: \"flex items-center gap-2 text-white/80 text-sm mb-2\" ,}\n , React.createElement(icons.BookOpen, { className: \"w-4 h-4\" ,} ), \"Lesson \"\n , activeLesson.id, \" of \" , lessons.length\n )\n , React.createElement('h2', { className: \"text-3xl font-bold mb-2\" ,}, activeLesson.title)\n , React.createElement('p', { className: \"text-white/90\",}, activeLesson.description)\n )\n\n /* Lesson Content */\n , React.createElement('div', { className: \"p-8\",}\n , React.createElement('div', { className: \"prose max-w-none mb-8\" ,}\n , React.createElement('div', { className: \"flex items-start gap-3 p-4 bg-blue-50 rounded-xl border border-blue-100 mb-6\" ,}\n , React.createElement(icons.Lightbulb, { className: \"w-6 h-6 text-blue-500 flex-shrink-0 mt-0.5\" ,} )\n , React.createElement('p', { className: \"text-gray-700 leading-relaxed\" ,}, activeLesson.content)\n )\n )\n\n , React.createElement('h3', { className: \"text-lg font-bold text-gray-800 mb-4 flex items-center gap-2\" ,}\n , React.createElement(icons.Terminal, { className: \"w-5 h-5 text-orange-500\" ,} ), \"Code Example\"\n\n )\n , React.createElement(CodeBlock, { code: activeLesson.code,} )\n\n /* Navigation */\n , React.createElement('div', { className: \"flex items-center justify-between mt-8 pt-6 border-t border-gray-100\" ,}\n , React.createElement('button', {\n onClick: () => {\n const prevIndex = lessons.findIndex(l => l.id === activeLesson.id) - 1\n if (prevIndex >= 0) setActiveLesson(lessons[prevIndex])\n },\n disabled: activeLesson.id === 1,\n className: \"flex items-center gap-2 px-4 py-2 rounded-lg text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\" ,}\n\n , React.createElement(icons.ChevronLeft, { className: \"w-5 h-5\" ,} ), \"Previous\"\n\n )\n , React.createElement('span', { className: \"text-sm text-gray-500\" ,}\n , activeLesson.id, \" / \" , lessons.length\n )\n , React.createElement('button', {\n onClick: () => {\n const nextIndex = lessons.findIndex(l => l.id === activeLesson.id) + 1\n if (nextIndex < lessons.length) setActiveLesson(lessons[nextIndex])\n },\n disabled: activeLesson.id === lessons.length,\n className: \"flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-orange-500 to-pink-500 text-white hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed transition-opacity\" ,}\n, \"Next\"\n\n , React.createElement(icons.ChevronRight, { className: \"w-5 h-5\" ,} )\n )\n )\n )\n )\n )\n )\n )\n\n /* Footer */\n , React.createElement('footer', { className: \"bg-white border-t border-gray-200 mt-12 py-6\" ,}\n , React.createElement('div', { className: \"max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm\" ,}\n , React.createElement('p', null, \"🚀 Start your web development journey with HTML Academy\" )\n )\n )\n )\n )\n} exports.default = HTMLLessons;"; // Execute component try { var module = { exports: {} }; var exports = module.exports; // Make all necessary functions available in the eval scope var useState = React.useState; var useEffect = React.useEffect; var useCallback = React.useCallback; var useMemo = React.useMemo; var useRef = React.useRef; var useContext = React.useContext; var useReducer = React.useReducer; var Fragment = React.Fragment; // Make recharts components available as globals var ResponsiveContainer = window.recharts.ResponsiveContainer; var LineChart = window.recharts.LineChart; var BarChart = window.recharts.BarChart; var PieChart = window.recharts.PieChart; var AreaChart = window.recharts.AreaChart || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'AreaChart'); }; var ScatterChart = window.recharts.ScatterChart || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'ScatterChart'); }; var ComposedChart = window.recharts.ComposedChart || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'ComposedChart'); }; var RadarChart = window.recharts.RadarChart || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'RadarChart'); }; var RadialBarChart = window.recharts.RadialBarChart || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'RadialBarChart'); }; var Treemap = window.recharts.Treemap || function(props) { return React.createElement('div', { className: 'chart-placeholder' }, props.children || 'Treemap'); }; var Line = window.recharts.Line; var Bar = window.recharts.Bar; var Area = window.recharts.Area || 'area'; var Scatter = window.recharts.Scatter || 'scatter'; var Pie = window.recharts.Pie || 'pie'; var Radar = window.recharts.Radar || 'radar'; var RadialBar = window.recharts.RadialBar || 'radialbar'; var XAxis = window.recharts.XAxis; var YAxis = window.recharts.YAxis; var ZAxis = window.recharts.ZAxis || 'z-axis'; var CartesianGrid = window.recharts.CartesianGrid || 'cartesian-grid'; var Tooltip = window.recharts.Tooltip; var Legend = window.recharts.Legend; var Cell = window.recharts.Cell || 'cell'; var ReferenceLine = window.recharts.ReferenceLine || 'reference-line'; var ReferenceArea = window.recharts.ReferenceArea || 'reference-area'; var ReferenceDot = window.recharts.ReferenceDot || 'reference-dot'; var Brush = window.recharts.Brush || 'brush'; var PolarGrid = window.recharts.PolarGrid || 'polar-grid'; var PolarAngleAxis = window.recharts.PolarAngleAxis || 'polar-angle-axis'; var PolarRadiusAxis = window.recharts.PolarRadiusAxis || 'polar-radius-axis'; // Mock require for browser - this is what the transformed code will call function require(moduleName) { console.log('[JustCopy] Requiring module:', moduleName); if (moduleName === 'react') { return window.React; } if (moduleName === 'lucide-react') { // Return our mock Lucide icons console.log('[JustCopy] Returning mock Lucide icons'); return LucideIcons; } if (moduleName === 'framer-motion') { return window.framerMotion || {}; } if (moduleName === 'react-hook-form') { return window.reactHookForm || {}; } if (moduleName === 'zod') { return window.zod || {}; } if (moduleName === 'date-fns') { return window.dateFns || {}; } if (moduleName === 'recharts') { return window.recharts || {}; } return {}; } // Make require available globally for eval window.require = require; // Provide database API that calls runtime backend // Determine runtime service URL based on environment var API_BASE = 'https://api.justcopy.link'; var getProjectId = function() { return '77220a0f-bd2a-44a6-9bdd-3014eb48fe4d'; }; // Get current user ID from Cognito auth // Returns the Cognito sub (user ID) if logged in // This is an async function since getCurrentUser() is async var getUserId = async function() { // Check if auth capability is enabled and user is authenticated if (window.auth && window.auth.getCurrentUser) { var user = await window.auth.getCurrentUser(); // getCurrentUser returns { id: sub, email, name, picture } if (user && user.id) { return user.id; } } // Not authenticated - return null (API will reject unauthenticated requests) return null; }; // SECURITY: Get auth token for API requests // Returns the JWT token from Cognito session or Parent Window bridge var getAuthToken = async function() { // 1. Check for token injected via postMessage bridge (Preview Mode) if (window.__AUTH_TOKEN__) { return window.__AUTH_TOKEN__; } // 2. Check standard auth capability if (!window.auth) { console.error('[JustCopy DB] Authentication is not configured. Enable the "auth" capability in your project settings.'); return null; } if (!window.auth.getToken) { console.error('[JustCopy DB] Auth module does not have getToken method.'); return null; } try { var token = await window.auth.getToken(); if (!token) { console.warn('[JustCopy DB] No auth token available. User may not be logged in.'); } return token; } catch (e) { console.error('[JustCopy DB] Failed to get auth token:', e); return null; } }; // SETUP AUTH BRIDGE: Listen for token from parent window window.addEventListener('message', function(event) { if (event.data && event.data.type === 'AUTH_TOKEN') { console.log('[JustCopy] Received auth token from parent'); window.__AUTH_TOKEN__ = event.data.token; } }); // Request token immediately if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'REQUEST_AUTH_TOKEN' }, '*'); } // Helper to check if auth is properly configured var isAuthConfigured = function() { return !!(window.auth && window.auth.getToken && window.auth.getCurrentUser); }; // Helper to build headers with authentication var buildHeaders = async function(projectId) { var headers = { 'Content-Type': 'application/json', 'X-Project-Id': projectId }; var token = await getAuthToken(); if (token) { headers['Authorization'] = 'Bearer ' + token; } return headers; }; // Real backend API // SECURITY: All database operations now require authentication window.db = { async create(entity, data) { var projectId = getProjectId(); // Check if auth is configured if (!isAuthConfigured()) { throw new Error('Database requires Authentication capability. Please enable "auth" in your project settings, then add a login page for your users.'); } var token = await getAuthToken(); if (!token) throw new Error('Please log in to use the database. Use window.auth.login() or add a login page.'); var res = await fetch(API_BASE + '/api/customer-backend/db/create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ projectId: projectId, entity: entity, data: data }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to create' }; }); throw new Error(err.error || err.message || 'Failed to create'); } return res.json(); }, async query(entity, options) { var projectId = getProjectId(); if (!isAuthConfigured()) { throw new Error('Database requires Authentication capability. Please enable "auth" in your project settings, then add a login page for your users.'); } var token = await getAuthToken(); if (!token) throw new Error('Please log in to use the database. Use window.auth.login() or add a login page.'); var res = await fetch(API_BASE + '/api/customer-backend/db/query', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ projectId: projectId, entity: entity, ...(options || {}) }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to query' }; }); throw new Error(err.error || err.message || 'Failed to query'); } return res.json(); }, async update(entity, recordId, data) { var projectId = getProjectId(); if (!isAuthConfigured()) { throw new Error('Database requires Authentication capability. Please enable "auth" in your project settings, then add a login page for your users.'); } var token = await getAuthToken(); if (!token) throw new Error('Please log in to use the database. Use window.auth.login() or add a login page.'); var res = await fetch(API_BASE + '/api/customer-backend/db/' + entity + '/' + recordId, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ projectId: projectId, data: data }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to update' }; }); throw new Error(err.error || err.message || 'Failed to update'); } return res.json(); }, async delete(entity, recordId) { var projectId = getProjectId(); if (!isAuthConfigured()) { throw new Error('Database requires Authentication capability. Please enable "auth" in your project settings, then add a login page for your users.'); } var token = await getAuthToken(); if (!token) throw new Error('Please log in to use the database. Use window.auth.login() or add a login page.'); var res = await fetch(API_BASE + '/api/customer-backend/db/' + entity + '/' + recordId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ projectId: projectId }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to delete' }; }); throw new Error(err.error || err.message || 'Failed to delete'); } return res.json(); }, async get(entity, recordId) { var projectId = getProjectId(); if (!isAuthConfigured()) { throw new Error('Database requires Authentication capability. Please enable "auth" in your project settings, then add a login page for your users.'); } var token = await getAuthToken(); if (!token) throw new Error('Please log in to use the database. Use window.auth.login() or add a login page.'); var res = await fetch(API_BASE + '/api/customer-backend/db/' + entity + '/' + recordId, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + token } }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to get record' }; }); throw new Error(err.error || err.message || 'Failed to get record'); } return res.json(); }, // Dexie-style compatibility layer todos: { toArray: function() { return window.db.query('todos'); }, add: function(item) { return window.db.create('todos', item); }, create: function(item) { return window.db.create('todos', item); }, put: function(item) { return window.db.create('todos', item); }, update: function(id, changes) { return window.db.update('todos', id, changes); }, delete: function(id) { return window.db.delete('todos', id); }, where: function() { return { toArray: function() { return window.db.query('todos'); } }; } } }; // ============================================ // Integrations API (window.api.integrations) // Supports: Notion, Slack, Google Calendar/Drive/Docs/Slides/Sheets // Only initialized when integrations capability is enabled // ============================================ // integrations capability not enabled // ============================================ // File Storage API (window.storage) // Only initialized when storage capability is enabled // ============================================ window.storage = { // Helper to show error banner in UI _showError: function(message, isAuthError) { // Remove any existing error banner var existingBanner = document.getElementById('justcopy-storage-error-banner'); if (existingBanner) existingBanner.remove(); // Create error banner var banner = document.createElement('div'); banner.id = 'justcopy-storage-error-banner'; banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;padding:16px 20px;background:linear-gradient(135deg,#dc2626,#b91c1c);color:white;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 4px 12px rgba(0,0,0,0.15);'; var content = document.createElement('div'); content.style.cssText = 'display:flex;align-items:center;gap:12px;flex:1;'; var icon = document.createElement('span'); icon.innerHTML = ''; var text = document.createElement('span'); text.textContent = message; content.appendChild(icon); content.appendChild(text); banner.appendChild(content); var closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = 'background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0 0 0 16px;opacity:0.8;'; closeBtn.onclick = function() { banner.remove(); }; banner.appendChild(closeBtn); document.body.insertBefore(banner, document.body.firstChild); // Auto-dismiss after 8 seconds for non-auth errors if (!isAuthError) { setTimeout(function() { if (banner.parentNode) banner.remove(); }, 8000); } }, async upload(file, options) { options = options || {}; var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to upload files.'; this._showError(msg, true); throw new Error(msg); } var formData = new FormData(); formData.append('file', file); formData.append('projectId', projectId); if (options.folder) formData.append('folder', options.folder); if (options.isPublic) formData.append('isPublic', 'true'); if (options.tags) formData.append('tags', JSON.stringify(options.tags)); if (options.metadata) formData.append('metadata', JSON.stringify(options.metadata)); var res = await fetch(API_BASE + '/api/customer-filesystem/upload', { method: 'POST', headers: { 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken }, body: formData }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to upload' }; }); throw new Error(err.error || err.message || 'Failed to upload'); } return res.json(); }, async list(options) { options = options || {}; var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to list files.'; this._showError(msg, true); throw new Error(msg); } var params = new URLSearchParams({ projectId: projectId }); if (options.folder) params.append('folder', options.folder); if (options.limit) params.append('limit', options.limit.toString()); if (options.cursor) params.append('cursor', options.cursor); var res = await fetch(API_BASE + '/api/customer-filesystem/files?' + params.toString(), { headers: { 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken } }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to list files' }; }); throw new Error(err.error || err.message || 'Failed to list files'); } return res.json(); }, async get(fileId) { var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to access files.'; this._showError(msg, true); throw new Error(msg); } var res = await fetch(API_BASE + '/api/customer-filesystem/files/' + fileId, { headers: { 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken } }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to get file' }; }); throw new Error(err.error || err.message || 'Failed to get file'); } return res.json(); }, async delete(fileId, options) { options = options || {}; var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to delete files.'; this._showError(msg, true); throw new Error(msg); } var url = API_BASE + '/api/customer-filesystem/files/' + fileId; if (options.hard) url += '?hard=true'; var res = await fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken }, body: JSON.stringify({ projectId: projectId }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to delete' }; }); throw new Error(err.error || err.message || 'Failed to delete'); } return res.json(); }, async getSignedUrl(fileId, expires) { expires = expires || 3600; var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to access files.'; this._showError(msg, true); throw new Error(msg); } var res = await fetch(API_BASE + '/api/customer-filesystem/signed-url', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken }, body: JSON.stringify({ projectId: projectId, fileId: fileId, expires: expires }) }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to get signed URL' }; }); throw new Error(err.error || err.message || 'Failed to get signed URL'); } return res.json(); }, async getQuota() { var projectId = getProjectId(); if (!isAuthConfigured()) { var msg = 'File Storage requires Authentication. Go to Capabilities tab in Studio and enable "Authentication", then add a login page to your app.'; this._showError(msg, true); throw new Error(msg); } var authToken = await getAuthToken(); if (!authToken) { var msg = 'Please log in to check quota.'; this._showError(msg, true); throw new Error(msg); } var res = await fetch(API_BASE + '/api/customer-filesystem/quota', { headers: { 'X-Project-Id': projectId, 'Authorization': 'Bearer ' + authToken } }); if (!res.ok) { var err = await res.json().catch(function() { return { error: 'Failed to get quota' }; }); throw new Error(err.error || err.message || 'Failed to get quota'); } return res.json(); } }; // ============================================ // AI Chatbot API (window.chat) // Only initialized when ai-agents or voice-ai-agent capability is enabled // ============================================ // AI Chatbot capability not enabled - window.chat not available // ============================================ // AI Agent API (window.agent) // Full agentic AI with tool calling, commentary, and multi-step execution // Only initialized when ai-agents capability is enabled // ============================================ // AI Agent capability not enabled - window.agent not available // ============================================ // Voice AI API (window.voice) // Only initialized when voice-ai-agent capability is enabled // ============================================ // Voice AI capability not enabled - window.voice not available // ============================================ // Authentication API using AWS Amplify v5 CDN // Only initialized when auth capability is enabled // ============================================ (function() { // ============================================ // Navigation helper for auth pages // Handles preview path prefix on localhost // ============================================ var getBasePath = function() { // Check if we're on a preview path (localhost) var path = window.location.pathname; var previewMatch = path.match(/^\/preview\/([^/]+)/); if (previewMatch) { return '/preview/' + previewMatch[1]; } return ''; }; // Navigation helper that works on both production and preview window.authNavigate = function(targetPath) { var basePath = getBasePath(); window.location.href = basePath + targetPath; }; // Get current auth page from URL window.getCurrentAuthPage = function() { var path = window.location.pathname; var basePath = getBasePath(); if (basePath) { // Remove base path to get the auth page var authPath = path.replace(basePath, ''); return authPath || '/'; } return path; }; console.log('[JustCopy Auth] Navigation helper initialized, base path:', getBasePath()); // Configure Amplify with JustCopy's Cognito settings var amplifyCore = window.aws_amplify_core; var amplifyAuth = window.aws_amplify_auth; if (amplifyCore && amplifyCore.Amplify) { // Determine OAuth redirect URL based on domain type: // IMPORTANT: AWS Cognito does NOT support wildcard callback URLs // So ALL subdomains (*.justcopy.link) must route through oauth.justcopy.link // Only localhost is allowed to use direct origin var hostname = window.location.hostname; var isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'; var oauthRedirectUrl; if (isLocalhost) { // Localhost is explicitly registered in Cognito callback URLs oauthRedirectUrl = window.location.origin + '/'; } else { // ALL production domains (*.justcopy.link, *.justcopy.ai, custom domains) // must proxy through oauth.justcopy.link since Cognito doesn't support wildcards oauthRedirectUrl = 'https://oauth.justcopy.link/'; } amplifyCore.Amplify.configure({ Auth: { region: 'us-east-1', userPoolId: 'us-east-1_CVEfKuDvr', userPoolWebClientId: '5904h0sgac1im0barl40tpqkrb', identityPoolId: 'us-east-1:de7e390d-aa77-40f8-9dac-06ad088e5bc2', oauth: { domain: 'auth.justcopy.ai', scope: ['email', 'profile', 'openid'], redirectSignIn: oauthRedirectUrl, redirectSignOut: oauthRedirectUrl, responseType: 'code' } } }); console.log('[JustCopy Auth] Amplify configured with redirect:', oauthRedirectUrl); // Check if we're on an OAuth callback (URL has code parameter) // If so, Amplify will automatically exchange the code for tokens var urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code')) { console.log('[JustCopy Auth] OAuth callback detected, Amplify will handle token exchange'); } // Check if we have tokens from OAuth proxy (custom domain flow) // The OAuth proxy redirects here with tokens in the URL fragment if (window.location.hash.includes('auth_tokens=')) { try { var hash = window.location.hash.substring(1); var tokenParam = hash.split('auth_tokens=')[1]; if (tokenParam) { var tokenData = JSON.parse(decodeURIComponent(tokenParam.split('&')[0])); console.log('[JustCopy Auth] Received tokens from OAuth proxy'); // Store tokens in Amplify's expected format var userPoolId = 'us-east-1_CVEfKuDvr'; var clientId = '5904h0sgac1im0barl40tpqkrb'; var keyPrefix = 'CognitoIdentityServiceProvider.' + clientId; var lastAuthUser = tokenData.email || tokenData.sub; localStorage.setItem(keyPrefix + '.LastAuthUser', lastAuthUser); localStorage.setItem(keyPrefix + '.' + lastAuthUser + '.idToken', tokenData.idToken); localStorage.setItem(keyPrefix + '.' + lastAuthUser + '.accessToken', tokenData.accessToken); localStorage.setItem(keyPrefix + '.' + lastAuthUser + '.refreshToken', tokenData.refreshToken); // Clear the hash from URL (for cleaner appearance) history.replaceState(null, '', window.location.pathname + window.location.search); console.log('[JustCopy Auth] Tokens stored, user should be authenticated'); } } catch (e) { console.error('[JustCopy Auth] Failed to process tokens from OAuth proxy:', e); } } } var Auth = amplifyAuth && amplifyAuth.Auth ? amplifyAuth.Auth : null; // Create window.auth API window.auth = { // Sign up a new user async signup(email, password, userData) { if (!Auth) return { success: false, error: 'Auth not available' }; try { // Only send email attribute - name/other attributes can cause permission issues // Users can update their profile after signup var result = await Auth.signUp({ username: email, password: password, attributes: { email: email } }); return { success: true, user: { email: email, id: result.userSub }, needsConfirmation: !result.userConfirmed }; } catch (error) { console.error('[JustCopy Auth] Signup error:', error); // Map Cognito error codes to user-friendly messages var errorMessage = 'Signup failed. Please try again.'; var errorCode = error.code || error.name || ''; if (errorCode === 'UsernameExistsException' || error.message?.includes('already exists')) { errorMessage = 'An account with this email already exists. Please sign in instead.'; } else if (errorCode === 'InvalidPasswordException' || error.message?.includes('Password')) { errorMessage = 'Password must be at least 8 characters with uppercase, lowercase, and numbers.'; } else if (errorCode === 'InvalidParameterException' && error.message?.includes('email')) { errorMessage = 'Please enter a valid email address.'; } else if (errorCode === 'TooManyRequestsException' || error.message?.includes('Too many')) { errorMessage = 'Too many attempts. Please wait a moment and try again.'; } else if (error.message) { errorMessage = error.message; } return { success: false, error: errorMessage }; } }, // Confirm signup with verification code async confirmSignup(email, code) { if (!Auth) return { success: false, error: 'Auth not available' }; try { await Auth.confirmSignUp(email, code); return { success: true }; } catch (error) { console.error('[JustCopy Auth] Confirm signup error:', error); return { success: false, error: error.message || 'Confirmation failed' }; } }, // Helper to track user login for this project async _trackUserLogin(userData) { try { // Skip if no valid user data if (!userData || !userData.id || !userData.email) { console.warn('[JustCopy Auth] Skipping track login - missing user data'); return; } var runtimeApiUrl = 'https://api.justcopy.link'; await fetch(runtimeApiUrl + '/api/auth/track-login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Project-Id': '77220a0f-bd2a-44a6-9bdd-3014eb48fe4d' }, body: JSON.stringify({ userId: userData.id, email: userData.email, name: userData.name || '' }) }); // Also call analytics.identify() to register user in analytics if (window.analytics && window.analytics.identify) { await window.analytics.identify(userData.id, { email: userData.email, name: userData.name || '', source: 'auth_login' }); console.log('[JustCopy Auth] User identified for analytics:', userData.email); } } catch (e) { // Silent fail - tracking is non-critical console.warn('[JustCopy Auth] Failed to track login:', e); } }, // Sign in existing user async login(email, password) { if (!Auth) return { success: false, error: 'Auth not available' }; try { var user = await Auth.signIn(email, password); var attributes = user.attributes || {}; // Try to get name from various sources var userName = attributes.name || attributes.given_name || attributes['custom:name'] || (attributes.given_name && attributes.family_name ? attributes.given_name + ' ' + attributes.family_name : '') || (attributes.email ? attributes.email.split('@')[0] : ''); var userData = { id: attributes.sub, email: attributes.email || email, name: userName, picture: attributes.picture || '' }; // Track user login for this project this._trackUserLogin(userData); return { success: true, user: userData }; } catch (error) { console.error('[JustCopy Auth] Login error:', error); // Map Cognito error codes to user-friendly messages var errorMessage = 'Login failed. Please try again.'; var errorCode = error.code || error.name || ''; if (errorCode === 'UserNotFoundException' || error.message?.includes('User does not exist')) { errorMessage = 'No account found with this email. Please sign up first.'; } else if (errorCode === 'NotAuthorizedException' || error.message?.includes('Incorrect username or password')) { errorMessage = 'Incorrect email or password. Please try again.'; } else if (errorCode === 'UserNotConfirmedException') { errorMessage = 'Please verify your email before signing in.'; } else if (errorCode === 'PasswordResetRequiredException') { errorMessage = 'You need to reset your password. Please use forgot password.'; } else if (errorCode === 'TooManyRequestsException' || error.message?.includes('Too many')) { errorMessage = 'Too many attempts. Please wait a moment and try again.'; } else if (error.message) { errorMessage = error.message; } return { success: false, error: errorMessage }; } }, // Sign out async logout() { if (!Auth) return { success: false, error: 'Auth not available' }; try { await Auth.signOut(); return { success: true }; } catch (error) { console.error('[JustCopy Auth] Logout error:', error); return { success: false, error: error.message || 'Logout failed' }; } }, // Get current authenticated user async getCurrentUser() { if (!Auth) return null; try { var user = await Auth.currentAuthenticatedUser(); var attributes = user.attributes || {}; // For federated users (Google OAuth), attributes may be empty // Get user info from the ID token instead var email = attributes.email; var name = attributes.name || attributes.given_name || ''; var picture = attributes.picture || ''; var sub = attributes.sub; // Always try to get sub from ID token for federated users // The user.username for federated users is like "Google_123..." not the Cognito sub try { var session = await Auth.currentSession(); var idToken = session.getIdToken(); var payload = idToken.payload || {}; // ID token always has the real Cognito sub sub = payload.sub || sub || user.username; email = email || payload.email || ''; name = name || payload.name || payload.given_name || ''; picture = picture || payload.picture || ''; console.log('[JustCopy Auth] Got user info from ID token, sub:', sub); } catch (e) { console.warn('[JustCopy Auth] Could not get session:', e); // Fallback to user.username only if we couldn't get session sub = sub || user.username; } // Fallback: extract name from email if (!name && email) { name = email.split('@')[0]; } var userData = { id: sub, email: email, name: name, picture: picture }; console.log('[JustCopy Auth] User data:', JSON.stringify(userData)); // Track user activity for this project if (userData.email) { this._trackUserLogin(userData); } return userData; } catch (error) { console.error('[JustCopy Auth] getCurrentUser error:', error); return null; } }, // Check if user is authenticated async isAuthenticated() { if (!Auth) return false; try { await Auth.currentAuthenticatedUser(); return true; } catch (error) { return false; } }, // Sign in with Google (OAuth) async loginWithGoogle() { if (!Auth) return { success: false, error: 'Auth not available' }; try { // Save projectId to localStorage as backup localStorage.setItem('oauth_project_id', '77220a0f-bd2a-44a6-9bdd-3014eb48fe4d'); var hostname = window.location.hostname; var isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'; var isPreviewRoute = window.location.pathname.startsWith('/preview/'); // Determine redirect URI: // IMPORTANT: AWS Cognito does NOT support wildcard callback URLs // So ALL production domains (*.justcopy.link, custom domains) must proxy through oauth.justcopy.link // Only localhost can use direct origin since it's explicitly registered in Cognito var redirectUri = isLocalhost ? window.location.origin + '/' : 'https://oauth.justcopy.link/'; var oauthDomain = 'auth.justcopy.ai'; var clientId = '5904h0sgac1im0barl40tpqkrb'; var scope = encodeURIComponent('email profile openid'); // Determine the return URL after OAuth: // - If on /preview/{projectId} route, redirect back to same preview URL // - If on project subdomain or custom domain, use current origin // - If on localhost, use current origin var returnUrl; if (isLocalhost) { returnUrl = window.location.origin; } else if (isPreviewRoute) { // Redirect back to the same /preview/{projectId} URL returnUrl = window.location.origin + '/preview/77220a0f-bd2a-44a6-9bdd-3014eb48fe4d'; } else { // On subdomain or custom domain - use current origin returnUrl = window.location.origin; } // Encode return URL and projectId in the state parameter // This allows the OAuth callback to know where to redirect back var stateData = JSON.stringify({ returnUrl: returnUrl, projectId: '77220a0f-bd2a-44a6-9bdd-3014eb48fe4d' }); var state = encodeURIComponent(btoa(stateData)); var oauthUrl = 'https://' + oauthDomain + '/oauth2/authorize?' + 'identity_provider=Google' + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&response_type=code' + '&client_id=' + clientId + '&scope=' + scope + '&state=' + state; window.location.href = oauthUrl; return { success: true }; } catch (error) { console.error('[JustCopy Auth] Google login error:', error); return { success: false, error: error.message || 'Google login failed' }; } }, // Request password reset async forgotPassword(email) { if (!Auth) return { success: false, error: 'Auth not available' }; try { await Auth.forgotPassword(email); return { success: true }; } catch (error) { console.error('[JustCopy Auth] Forgot password error:', error); return { success: false, error: error.message || 'Failed to send reset code' }; } }, // Complete password reset with code async resetPassword(email, code, newPassword) { if (!Auth) return { success: false, error: 'Auth not available' }; try { await Auth.forgotPasswordSubmit(email, code, newPassword); return { success: true }; } catch (error) { console.error('[JustCopy Auth] Reset password error:', error); return { success: false, error: error.message || 'Password reset failed' }; } }, // Change password (when logged in) async changePassword(oldPassword, newPassword) { if (!Auth) return { success: false, error: 'Auth not available' }; try { var user = await Auth.currentAuthenticatedUser(); await Auth.changePassword(user, oldPassword, newPassword); return { success: true }; } catch (error) { console.error('[JustCopy Auth] Change password error:', error); return { success: false, error: error.message || 'Password change failed' }; } }, // Get session token (for API calls) async getToken() { if (!Auth) return null; try { var session = await Auth.currentSession(); return session.getIdToken().getJwtToken(); } catch (error) { return null; } } }; // Add method aliases for compatibility with Amplify naming conventions // This allows both auth.login() and auth.signIn() to work window.auth.signIn = window.auth.login; window.auth.signUp = window.auth.signup; window.auth.signOut = window.auth.logout; window.auth.confirmSignUp = window.auth.confirmSignup; console.log('[JustCopy Auth] window.auth API initialized with aliases'); // ============================================ // Pre-built Auth Page Components - Modern & Sleek Design // Available as window.AuthPages.Login, etc. // ============================================ window.AuthPages = { // Login Page Component - Modern Light Theme Login: function LoginPage(props) { // Client-only rendering to prevent hydration mismatch var _mounted = React.useState(false); var mounted = _mounted[0]; var setMounted = _mounted[1]; React.useEffect(function() { setMounted(true); }, []); var redirectTo = props?.redirectTo || '/dashboard'; var onSuccess = props?.onSuccess; var showSignupLink = props?.showSignupLink !== false; var showForgotPassword = props?.showForgotPassword !== false; var showGoogleLogin = props?.showGoogleLogin !== false; var title = props?.title || 'Welcome back'; var subtitle = props?.subtitle || 'Sign in to continue'; var _state = React.useState({ email: '', password: '', error: '', loading: false }); var state = _state[0]; var setState = _state[1]; // Return loading placeholder during SSR/hydration if (!mounted) { return React.createElement('div', { className: 'w-full max-w-xl mx-auto animate-pulse' }, React.createElement('div', { className: 'h-8 bg-zinc-200 rounded w-48 mx-auto mb-2' }), React.createElement('div', { className: 'h-4 bg-zinc-200 rounded w-32 mx-auto mb-6' }), React.createElement('div', { className: 'bg-zinc-100 rounded-xl p-6 space-y-4' }, React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }) ) ); } function updateState(updates) { setState(function(prev) { return Object.assign({}, prev, updates); }); } async function handleSubmit(e) { e.preventDefault(); updateState({ loading: true, error: '' }); var result = await window.auth.login(state.email, state.password); if (result.success) { if (onSuccess) { onSuccess(result.user); } else { window.authNavigate(redirectTo); } } else { updateState({ error: result.error, loading: false }); } } async function handleGoogleLogin() { await window.auth.loginWithGoogle(); } return React.createElement('div', { className: 'w-full max-w-xl mx-auto' }, React.createElement('div', { className: 'text-center mb-6' }, React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, title), React.createElement('p', { className: 'text-zinc-500 mt-1' }, subtitle) ), React.createElement('div', { className: 'bg-zinc-50 border border-zinc-200 rounded-xl p-6' }, state.error && React.createElement('div', { className: 'mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm' }, state.error), React.createElement('form', { onSubmit: handleSubmit, className: 'space-y-5' }, React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Email'), React.createElement('input', { type: 'email', value: state.email, onChange: function(e) { updateState({ email: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: 'you@example.com', required: true }) ), React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Password'), React.createElement('input', { type: 'password', value: state.password, onChange: function(e) { updateState({ password: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: '••••••••', required: true }) ), showForgotPassword && React.createElement('div', { className: 'text-right' }, React.createElement('button', { type: 'button', onClick: function() { window.authNavigate('/forgot-password'); }, className: 'text-sm text-blue-600 hover:text-blue-800 transition-colors' }, 'Forgot password?') ), React.createElement('button', { type: 'submit', disabled: state.loading, className: 'w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-300 disabled:text-zinc-500 text-white font-medium rounded-xl transition-all duration-200' }, state.loading ? React.createElement('span', { className: 'flex items-center justify-center gap-2' }, React.createElement('svg', { className: 'w-4 h-4 animate-spin', fill: 'none', viewBox: '0 0 24 24' }, React.createElement('circle', { className: 'opacity-25', cx: '12', cy: '12', r: '10', stroke: 'currentColor', strokeWidth: '4' }), React.createElement('path', { className: 'opacity-75', fill: 'currentColor', d: 'M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z' }) ), 'Signing in...' ) : 'Sign in') ), showGoogleLogin && React.createElement('div', null, React.createElement('div', { className: 'relative my-6' }, React.createElement('div', { className: 'absolute inset-0 flex items-center' }, React.createElement('div', { className: 'w-full border-t border-zinc-300' }) ), React.createElement('div', { className: 'relative flex justify-center text-sm' }, React.createElement('span', { className: 'px-4 bg-zinc-50 text-zinc-500' }, 'or') ) ), React.createElement('button', { type: 'button', onClick: handleGoogleLogin, className: 'w-full py-3 bg-white border border-zinc-300 rounded-xl flex items-center justify-center gap-3 hover:bg-zinc-50 hover:border-zinc-400 transition-all text-zinc-700' }, React.createElement('svg', { className: 'w-5 h-5', viewBox: '0 0 24 24' }, React.createElement('path', { fill: '#4285F4', d: 'M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z' }), React.createElement('path', { fill: '#34A853', d: 'M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z' }), React.createElement('path', { fill: '#FBBC05', d: 'M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z' }), React.createElement('path', { fill: '#EA4335', d: 'M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z' }) ), 'Continue with Google' ) ) ), showSignupLink && React.createElement('p', { className: 'mt-8 text-center text-zinc-500' }, "Don't have an account? ", React.createElement('button', { type: 'button', onClick: function() { window.authNavigate('/signup'); }, className: 'text-blue-600 hover:underline font-medium' }, 'Sign up') ) ); }, // Signup Page Component - Modern Light Theme Signup: function SignupPage(props) { // Client-only rendering to prevent hydration mismatch var _mounted = React.useState(false); var mounted = _mounted[0]; var setMounted = _mounted[1]; React.useEffect(function() { setMounted(true); }, []); var redirectTo = props?.redirectTo || '/dashboard'; var onSuccess = props?.onSuccess; var showLoginLink = props?.showLoginLink !== false; var showGoogleLogin = props?.showGoogleLogin !== false; var title = props?.title || 'Create account'; var subtitle = props?.subtitle || 'Start building today'; var _state = React.useState({ step: 'signup', name: '', email: '', password: '', code: '', error: '', loading: false }); var state = _state[0]; var setState = _state[1]; // Return loading placeholder during SSR/hydration if (!mounted) { return React.createElement('div', { className: 'w-full max-w-xl mx-auto animate-pulse' }, React.createElement('div', { className: 'h-8 bg-zinc-200 rounded w-48 mx-auto mb-2' }), React.createElement('div', { className: 'h-4 bg-zinc-200 rounded w-32 mx-auto mb-6' }), React.createElement('div', { className: 'bg-zinc-100 rounded-xl p-6 space-y-4' }, React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }) ) ); } function updateState(updates) { setState(function(prev) { return Object.assign({}, prev, updates); }); } async function handleSignup(e) { e.preventDefault(); updateState({ loading: true, error: '' }); var result = await window.auth.signup(state.email, state.password, { name: state.name }); if (result.success) { if (result.needsConfirmation) { updateState({ step: 'verify', loading: false }); } else { if (onSuccess) { onSuccess(result.user); } else { window.authNavigate(redirectTo); } } } else { updateState({ error: result.error, loading: false }); } } async function handleVerify(e) { e.preventDefault(); updateState({ loading: true, error: '' }); var result = await window.auth.confirmSignup(state.email, state.code); if (result.success) { var loginResult = await window.auth.login(state.email, state.password); if (loginResult.success) { if (onSuccess) { onSuccess(loginResult.user); } else { window.authNavigate(redirectTo); } } } else { updateState({ error: result.error, loading: false }); } } async function handleGoogleLogin() { await window.auth.loginWithGoogle(); } if (state.step === 'verify') { return React.createElement('div', { className: 'w-full max-w-xl mx-auto' }, React.createElement('div', { className: 'w-full' }, React.createElement('div', { className: 'text-center mb-8' }, React.createElement('div', { className: 'w-16 h-16 bg-emerald-500/10 border border-emerald-500/20 rounded-2xl flex items-center justify-center mx-auto mb-6' }, React.createElement('svg', { className: 'w-8 h-8 text-emerald-400', fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, React.createElement('path', { strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: 2, d: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' }) ) ), React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, 'Check your email'), React.createElement('p', { className: 'text-zinc-500 mt-1' }, 'We sent a code to ', React.createElement('span', { className: 'text-zinc-900 font-medium' }, state.email)) ), React.createElement('div', { className: 'bg-zinc-50 border border-zinc-200 rounded-xl p-6' }, state.error && React.createElement('div', { className: 'mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm' }, state.error), React.createElement('form', { onSubmit: handleVerify, className: 'space-y-5' }, React.createElement('input', { type: 'text', value: state.code, onChange: function(e) { updateState({ code: e.target.value }); }, className: 'w-full px-4 py-4 bg-white border border-zinc-300 rounded-xl text-center text-2xl tracking-[0.5em] text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all font-mono', placeholder: '000000', maxLength: 6, required: true }), React.createElement('button', { type: 'submit', disabled: state.loading, className: 'w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-300 disabled:text-zinc-500 text-white font-medium rounded-xl transition-all duration-200' }, state.loading ? 'Verifying...' : 'Verify email') ), React.createElement('button', { type: 'button', onClick: function() { updateState({ step: 'signup', code: '', error: '' }); }, className: 'w-full mt-4 text-zinc-500 hover:text-zinc-900 text-sm transition-colors' }, '← Back to signup') ) ) ); } return React.createElement('div', { className: 'w-full max-w-xl mx-auto' }, React.createElement('div', { className: 'text-center mb-6' }, React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, title), React.createElement('p', { className: 'text-zinc-500 mt-1' }, subtitle) ), React.createElement('div', { className: 'bg-zinc-50 border border-zinc-200 rounded-xl p-6' }, state.error && React.createElement('div', { className: 'mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm' }, state.error), React.createElement('form', { onSubmit: handleSignup, className: 'space-y-5' }, React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Name'), React.createElement('input', { type: 'text', value: state.name, onChange: function(e) { updateState({ name: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: 'Your name' }) ), React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Email'), React.createElement('input', { type: 'email', value: state.email, onChange: function(e) { updateState({ email: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: 'you@example.com', required: true }) ), React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Password'), React.createElement('input', { type: 'password', value: state.password, onChange: function(e) { updateState({ password: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: '••••••••', required: true, minLength: 8 }), React.createElement('p', { className: 'text-xs text-zinc-500 mt-2' }, 'At least 8 characters') ), React.createElement('button', { type: 'submit', disabled: state.loading, className: 'w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-300 disabled:text-zinc-500 text-white font-medium rounded-xl transition-all duration-200' }, state.loading ? 'Creating account...' : 'Create account') ), showGoogleLogin && React.createElement('div', null, React.createElement('div', { className: 'relative my-6' }, React.createElement('div', { className: 'absolute inset-0 flex items-center' }, React.createElement('div', { className: 'w-full border-t border-zinc-300' }) ), React.createElement('div', { className: 'relative flex justify-center text-sm' }, React.createElement('span', { className: 'px-4 bg-zinc-50 text-zinc-500' }, 'or') ) ), React.createElement('button', { type: 'button', onClick: handleGoogleLogin, className: 'w-full py-3 bg-white border border-zinc-300 rounded-xl flex items-center justify-center gap-3 hover:bg-zinc-50 hover:border-zinc-400 transition-all text-zinc-700' }, React.createElement('svg', { className: 'w-5 h-5', viewBox: '0 0 24 24' }, React.createElement('path', { fill: '#4285F4', d: 'M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z' }), React.createElement('path', { fill: '#34A853', d: 'M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z' }), React.createElement('path', { fill: '#FBBC05', d: 'M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z' }), React.createElement('path', { fill: '#EA4335', d: 'M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z' }) ), 'Continue with Google' ) ) ), showLoginLink && React.createElement('p', { className: 'mt-8 text-center text-zinc-500' }, 'Already have an account? ', React.createElement('button', { type: 'button', onClick: function() { window.authNavigate('/login'); }, className: 'text-blue-600 hover:underline font-medium' }, 'Sign in') ) ); }, // Forgot Password Page Component - Modern Light Theme ForgotPassword: function ForgotPasswordPage(props) { // Client-only rendering to prevent hydration mismatch var _mounted = React.useState(false); var mounted = _mounted[0]; var setMounted = _mounted[1]; React.useEffect(function() { setMounted(true); }, []); var onSuccess = props?.onSuccess; var showLoginLink = props?.showLoginLink !== false; var title = props?.title || 'Reset password'; var subtitle = props?.subtitle || "Enter your email to receive a reset code"; var _state = React.useState({ step: 'email', email: '', code: '', newPassword: '', error: '', loading: false, success: false }); var state = _state[0]; var setState = _state[1]; // Return loading placeholder during SSR/hydration if (!mounted) { return React.createElement('div', { className: 'w-full max-w-xl mx-auto animate-pulse' }, React.createElement('div', { className: 'h-8 bg-zinc-200 rounded w-48 mx-auto mb-2' }), React.createElement('div', { className: 'h-4 bg-zinc-200 rounded w-56 mx-auto mb-6' }), React.createElement('div', { className: 'bg-zinc-100 rounded-xl p-6 space-y-4' }, React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }) ) ); } function updateState(updates) { setState(function(prev) { return Object.assign({}, prev, updates); }); } async function handleSendCode(e) { e.preventDefault(); updateState({ loading: true, error: '' }); var result = await window.auth.forgotPassword(state.email); if (result.success) { updateState({ step: 'reset', loading: false }); } else { updateState({ error: result.error, loading: false }); } } async function handleResetPassword(e) { e.preventDefault(); updateState({ loading: true, error: '' }); var result = await window.auth.resetPassword(state.email, state.code, state.newPassword); if (result.success) { updateState({ success: true, loading: false }); if (onSuccess) { onSuccess(); } else { setTimeout(function() { window.authNavigate('/login'); }, 2000); } } else { updateState({ error: result.error, loading: false }); } } if (state.success) { return React.createElement('div', { className: 'w-full max-w-xl mx-auto text-center' }, React.createElement('div', { className: 'w-full' }, React.createElement('div', { className: 'w-16 h-16 bg-emerald-500/10 border border-emerald-500/20 rounded-2xl flex items-center justify-center mx-auto mb-6' }, React.createElement('svg', { className: 'w-8 h-8 text-emerald-400', fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' }, React.createElement('path', { strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: 2, d: 'M5 13l4 4L19 7' }) ) ), React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, 'Password reset!'), React.createElement('p', { className: 'text-zinc-500 mt-1' }, 'Redirecting to login...') ) ); } if (state.step === 'reset') { return React.createElement('div', { className: 'w-full max-w-xl mx-auto' }, React.createElement('div', { className: 'w-full' }, React.createElement('div', { className: 'text-center mb-8' }, React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, 'Enter reset code'), React.createElement('p', { className: 'text-zinc-500 mt-1' }, 'We sent a code to ', React.createElement('span', { className: 'text-zinc-900 font-medium' }, state.email)) ), React.createElement('div', { className: 'bg-zinc-50 border border-zinc-200 rounded-xl p-6' }, state.error && React.createElement('div', { className: 'mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm' }, state.error), React.createElement('form', { onSubmit: handleResetPassword, className: 'space-y-5' }, React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Reset code'), React.createElement('input', { type: 'text', value: state.code, onChange: function(e) { updateState({ code: e.target.value }); }, className: 'w-full px-4 py-4 bg-white border border-zinc-300 rounded-xl text-center text-2xl tracking-[0.5em] text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all font-mono', placeholder: '000000', required: true }) ), React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'New password'), React.createElement('input', { type: 'password', value: state.newPassword, onChange: function(e) { updateState({ newPassword: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: '••••••••', required: true, minLength: 8 }) ), React.createElement('button', { type: 'submit', disabled: state.loading, className: 'w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-300 disabled:text-zinc-500 text-white font-medium rounded-xl transition-all duration-200' }, state.loading ? 'Resetting...' : 'Reset password') ), React.createElement('button', { type: 'button', onClick: function() { updateState({ step: 'email', code: '', newPassword: '', error: '' }); }, className: 'w-full mt-4 text-zinc-500 hover:text-zinc-900 text-sm transition-colors' }, '← Back') ) ) ); } return React.createElement('div', { className: 'w-full max-w-xl mx-auto' }, React.createElement('div', { className: 'text-center mb-6' }, React.createElement('h1', { className: 'text-2xl font-bold text-zinc-900 tracking-tight' }, title), React.createElement('p', { className: 'text-zinc-500 mt-1' }, subtitle) ), React.createElement('div', { className: 'bg-zinc-50 border border-zinc-200 rounded-xl p-6' }, state.error && React.createElement('div', { className: 'mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm' }, state.error), React.createElement('form', { onSubmit: handleSendCode, className: 'space-y-5' }, React.createElement('div', null, React.createElement('label', { className: 'block text-sm font-medium text-zinc-700 mb-2' }, 'Email'), React.createElement('input', { type: 'email', value: state.email, onChange: function(e) { updateState({ email: e.target.value }); }, className: 'w-full px-4 py-3 bg-white border border-zinc-300 rounded-xl text-zinc-900 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all', placeholder: 'you@example.com', required: true }) ), React.createElement('button', { type: 'submit', disabled: state.loading, className: 'w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-300 disabled:text-zinc-500 text-white font-medium rounded-xl transition-all duration-200' }, state.loading ? 'Sending...' : 'Send reset code') ) ), showLoginLink && React.createElement('p', { className: 'mt-8 text-center text-zinc-500' }, 'Remember your password? ', React.createElement('button', { type: 'button', onClick: function() { window.authNavigate('/login'); }, className: 'text-blue-600 hover:underline font-medium' }, 'Sign in') ) ); }, // Auth Header Component - Modern Dark Theme AuthHeader: function AuthHeaderComponent(props) { var loginPath = props?.loginPath || '/login'; var signupPath = props?.signupPath || '/signup'; var onLogout = props?.onLogout; var showUserMenu = props?.showUserMenu !== false; var _user = React.useState(null); var user = _user[0]; var setUser = _user[1]; var _loading = React.useState(true); var loading = _loading[0]; var setLoading = _loading[1]; var _menuOpen = React.useState(false); var menuOpen = _menuOpen[0]; var setMenuOpen = _menuOpen[1]; React.useEffect(function() { checkAuth(); }, []); async function checkAuth() { var currentUser = await window.auth.getCurrentUser(); setUser(currentUser); setLoading(false); } async function handleLogout() { await window.auth.logout(); setUser(null); if (onLogout) { onLogout(); } else { window.authNavigate(loginPath); } } if (loading) { return React.createElement('div', { className: 'flex items-center gap-2' }, React.createElement('div', { className: 'w-8 h-8 bg-zinc-800 rounded-full animate-pulse' }) ); } if (user) { return React.createElement('div', { className: 'relative' }, React.createElement('button', { onClick: function() { setMenuOpen(!menuOpen); }, className: 'flex items-center gap-2 px-3 py-2 rounded-xl hover:bg-zinc-800/50 transition-colors' }, React.createElement('div', { className: 'w-8 h-8 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-full flex items-center justify-center text-white text-sm font-medium' }, (user.name || user.email || '?').charAt(0).toUpperCase() ), showUserMenu && React.createElement('span', { className: 'text-sm text-zinc-300 hidden sm:block' }, user.name || user.email?.split('@')[0]) ), menuOpen && React.createElement('div', { className: 'absolute right-0 mt-2 w-48 bg-zinc-900 border border-zinc-800 rounded-xl shadow-2xl py-1 z-50' }, React.createElement('div', { className: 'px-4 py-2.5 text-xs text-zinc-500 truncate' }, user.email || user.name), React.createElement('hr', { className: 'my-1 border-zinc-800' }), React.createElement('button', { onClick: handleLogout, className: 'w-full text-left px-4 py-2.5 text-sm text-red-400 hover:bg-zinc-800 hover:text-red-300 transition-colors' }, 'Sign out') ) ); } return React.createElement('div', { className: 'flex items-center gap-3' }, React.createElement('button', { onClick: function() { window.authNavigate(loginPath); }, className: 'px-4 py-2 text-sm font-medium text-zinc-600 hover:text-zinc-900 transition-colors' }, 'Sign in'), React.createElement('button', { onClick: function() { window.authNavigate(signupPath); }, className: 'px-5 py-2 text-sm font-medium bg-white hover:bg-zinc-100 text-zinc-900 rounded-full transition-colors' }, 'Sign up') ); } }; // AuthRouter - Automatically renders the correct auth page based on URL // Usage: if (!user) return window.AuthPages.Router = function AuthRouter(props) { // Client-only rendering to prevent hydration mismatch var _mounted = React.useState(false); var mounted = _mounted[0]; var setMounted = _mounted[1]; React.useEffect(function() { setMounted(true); }, []); var onSuccess = props?.onSuccess; var redirectTo = props?.redirectTo || '/'; // Return loading placeholder during SSR/hydration if (!mounted) { return React.createElement('div', { className: 'w-full max-w-xl mx-auto animate-pulse' }, React.createElement('div', { className: 'h-8 bg-zinc-200 rounded w-48 mx-auto mb-2' }), React.createElement('div', { className: 'h-4 bg-zinc-200 rounded w-32 mx-auto mb-6' }), React.createElement('div', { className: 'bg-zinc-100 rounded-xl p-6 space-y-4' }, React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }), React.createElement('div', { className: 'h-10 bg-zinc-200 rounded' }) ) ); } // Get the current page from URL (only runs on client after mount) var currentPage = window.getCurrentAuthPage ? window.getCurrentAuthPage() : window.location.pathname; console.log('[JustCopy Auth] AuthRouter rendering for path:', currentPage); // Match the path to the appropriate auth page if (currentPage === '/signup' || currentPage.endsWith('/signup')) { return React.createElement(window.AuthPages.Signup, { onSuccess: onSuccess, redirectTo: redirectTo }); } if (currentPage === '/forgot-password' || currentPage.endsWith('/forgot-password')) { return React.createElement(window.AuthPages.ForgotPassword, {}); } if (currentPage === '/reset-password' || currentPage.endsWith('/reset-password')) { return React.createElement(window.AuthPages.ForgotPassword, {}); } // Default to Login page for /login, /, or any other path return React.createElement(window.AuthPages.Login, { onSuccess: onSuccess, redirectTo: redirectTo }); }; console.log('[JustCopy Auth] Pre-built auth pages available: AuthPages.Login, AuthPages.Signup, AuthPages.ForgotPassword, AuthPages.AuthHeader, AuthPages.Router'); })(); // ============================================ // Pre-built Blog Components for SEO/AEO optimized blogs // Available as window.BlogComponents.* // ============================================ // Not a blog project - BlogComponents not available // Make React hooks available globally var useState = React.useState; var useEffect = React.useEffect; var useCallback = React.useCallback; var useMemo = React.useMemo; var useRef = React.useRef; var useContext = React.useContext; var createContext = React.createContext; // Create execution context with all needed variables // Important: db must be declared as a local variable for eval to access it (function(require, module, exports, React, window) { // Make React hooks available in the eval scope to fix _react.useState.call(void 0) errors var useState = React.useState; var useEffect = React.useEffect; var useCallback = React.useCallback; var useMemo = React.useMemo; var useRef = React.useRef; var useContext = React.useContext; var useReducer = React.useReducer; var useLayoutEffect = React.useLayoutEffect; var Fragment = React.Fragment; // Declare db in the local scope so eval can access it var db = window.db; // Declare storage in the local scope so eval can access it var storage = window.storage; // Auth is required when database or storage is enabled (window.db/window.storage require login) var auth = window.auth; var AuthPages = window.AuthPages; // not a blog project // chat not enabled // voice not enabled var integrations = window.api ? window.api.integrations : {}; // Make ALL Lucide icons available from the global LucideIcons object // Use the full list of known icon names (stored on window by the outer script) // so that icons not detected by extractUsedIcons still get injected via the Proxy fallback var iconDecls = (window.__allKnownIconNames || Object.keys(window.LucideIcons)).filter(function(k) { return /^[A-Z]/.test(k); }).map(function(k) { return 'var ' + k + ' = window.LucideIcons["' + k + '"];'; }).join('\n'); try { eval(iconDecls); } catch(e) { console.warn('Icon injection failed:', e); } // IMPORTANT: Make LucideIcons variable reference window.LucideIcons (the Proxy) // This ensures destructuring like const { ShoppingBag } = LucideIcons works with fallbacks var LucideIcons = window.LucideIcons; // Specifically ensure the icons used in this component are available var Music = window.Music || window.LucideIcons.Music || function() { return null; }; var Play = window.Play || window.LucideIcons.Play || function() { return null; }; var Pause = window.Pause || window.LucideIcons.Pause || function() { return null; }; var SkipForward = window.SkipForward || window.LucideIcons.SkipForward || function() { return null; }; var SkipBack = window.SkipBack || window.LucideIcons.SkipBack || function() { return null; }; var Volume2 = window.Volume2 || window.LucideIcons.Volume2 || function() { return null; }; var Heart = window.Heart || window.LucideIcons.Heart || function() { return null; }; // Make icons available as an object for the new API var icons = window.LucideIcons; window.icons = icons; if (componentCode) { eval(componentCode); // After eval, check what was exported console.log('[JustCopy] After eval - module.exports:', module.exports); console.log('[JustCopy] After eval - exports:', exports); } else { console.log('[JustCopy] No component code to hydrate - SSR only'); } })(require, module, exports, window.React, window); var Component = module.exports.default || module.exports || exports.default || exports; console.log('[JustCopy] Component extracted:', Component); console.log('[JustCopy] Component type:', typeof Component); // FIX React #130: Check if export is object instead of function (same as server-side) if (typeof Component === 'object' && typeof Component !== 'function') { console.log('[JustCopy] Got object instead of function, searching for component:', Object.keys(Component)); // Look for component function within the object var componentKeys = Object.keys(Component).filter(function(key) { return typeof Component[key] === 'function' && (key === 'default' || key[0] === key[0].toUpperCase()); }); if (componentKeys.length > 0) { Component = Component[componentKeys[0]]; console.log('[JustCopy] Found component function:', componentKeys[0]); } else { console.error('[JustCopy] Export is an object but contains no component functions. Available keys:', Object.keys(Component).join(', ')); Component = null; // Set to null to trigger error handling below } } // CRITICAL FIX: Wrap component in error boundary var ErrorBoundary = function(props) { var hasError = React.useState(false)[0]; var setHasError = React.useState(false)[1]; var error = React.useState(null)[0]; var setError = React.useState(null)[1]; React.useEffect(function() { var errorHandler = function(event) { console.error('[JustCopy] Runtime error caught:', event.error); setHasError(true); setError(event.error); event.preventDefault(); }; window.addEventListener('error', errorHandler); return function() { window.removeEventListener('error', errorHandler); }; }, []); if (hasError) { return React.createElement('div', { style: { padding: '20px', margin: '20px', background: '#fee', border: '2px solid #fcc', borderRadius: '8px' } }, React.createElement('h2', null, '⚠️ Component Error'), React.createElement('p', null, error ? error.toString() : 'An error occurred'), React.createElement('button', { onClick: function() { var errMsg = 'Fix this error in the component:\n\nError: ' + (error ? error.toString() : 'Unknown error') + '\n\nThis error occurred in the preview. Please check for missing imports, undefined variables, or syntax issues.'; window.parent.postMessage({ type: 'SEND_TO_AGENT', message: errMsg }, '*'); }, style: { padding: '8px 16px', background: '#7c3aed', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', marginTop: '10px' } }, '🤖 Ask AI to Fix') ); } return props.children; }; // Validate component before hydration if (!Component) { console.error('[JustCopy] No component found to hydrate'); document.getElementById('root').innerHTML = '
Error: No component exported
'; } else if (typeof Component !== 'function') { console.error('[JustCopy] Component is not a function:', typeof Component, Component); // Additional debug information if (Component && typeof Component === 'object') { console.error('[JustCopy] Component object keys:', Object.keys(Component)); console.error('[JustCopy] Component object values:', Component); } document.getElementById('root').innerHTML = '
Error: Invalid component type (expected function, got ' + typeof Component + ')
'; } else { // Determine if we should hydrate or client-render var root = document.getElementById('root'); if (!root) { console.error('[JustCopy] No root element found'); return; } // Check if root has SSR content var hasSSRContent = root.innerHTML.trim().length > 0; // Skip hydration for apps with auth - auth components are client-only // and will cause hydration mismatches var hasAuth = typeof window.auth !== 'undefined'; if (hasAuth && hasSSRContent) { console.log('[JustCopy] Auth detected - using client-side rendering to avoid hydration mismatch'); hasSSRContent = false; // Force client-side rendering root.innerHTML = ''; // Clear SSR content } // Get component props from URL (e.g., ?slide=N or ?page=N for slides/documents, ?post=slug for blogs) var componentProps = {}; var urlParams = new URLSearchParams(window.location.search); var slideParam = urlParams.get('slide') || urlParams.get('page'); var postParam = urlParams.get('post'); // Also check for path-based blog post URL: /posts/:slug var pathPostMatch = window.location.pathname.match(/^\/posts\/([^\/]+)\/?$/); if (pathPostMatch) { postParam = pathPostMatch[1]; console.log('[JustCopy] Detected path-based post URL:', postParam); } if (slideParam !== null) { var pageValue = parseInt(slideParam, 10) || 0; componentProps.slideIndex = pageValue; // For slides componentProps.pageIndex = pageValue; // For documents console.log('[JustCopy] Passing slideIndex/pageIndex prop:', pageValue); } if (postParam !== null) { componentProps.postSlug = postParam; // For blogs console.log('[JustCopy] Passing postSlug prop:', postParam); } try { if (hasSSRContent) { // Root has SSR content, attempt hydration console.log('[JustCopy] SSR content detected, attempting hydration...'); if (typeof ReactDOM !== 'undefined' && ReactDOM.hydrateRoot) { ReactDOM.hydrateRoot( root, React.createElement(Component, componentProps) ); console.log('[JustCopy] App hydrated successfully'); } else { console.error('[JustCopy] ReactDOM.hydrateRoot not available'); } } else { // Root is empty, use client-side rendering console.log('[JustCopy] No SSR content, using client-side rendering...'); if (typeof ReactDOM !== 'undefined' && ReactDOM.createRoot) { var rootInstance = ReactDOM.createRoot(root); rootInstance.render( React.createElement(Component, componentProps) ); console.log('[JustCopy] Client-side render successful'); } else { console.error('[JustCopy] ReactDOM.createRoot not available'); } } } catch (error) { console.error('[JustCopy] Render error:', error); // Fallback to client-side render try { if (root && ReactDOM.createRoot) { root.innerHTML = ''; // Clear any content var rootInstance = ReactDOM.createRoot(root); rootInstance.render( React.createElement(Component, componentProps) ); console.log('[JustCopy] Fallback to client-side render successful'); } } catch (renderError) { console.error('[JustCopy] All render attempts failed:', renderError); root.innerHTML = '
Error: Failed to render component. Check console for details.
'; } } } } catch (e) { console.error('[JustCopy] Critical hydration error:', e); console.error('[JustCopy] Error stack:', e.stack); var root = document.getElementById('root'); if (root) { root.innerHTML = '

⚠️ Error

Failed to load component: ' + (e.message || '').replace(//g, '>') + '

'; var fixBtn = document.getElementById('jc-fix-btn'); if (fixBtn) { fixBtn.addEventListener('click', function() { var errMsg = 'Fix this error in the component:\n\nError: ' + (e.message || 'Unknown error') + '\n\nThis error occurred in the preview. Please check for missing imports, undefined variables, or syntax issues.'; window.parent.postMessage({ type: 'SEND_TO_AGENT', message: errMsg }, '*'); }); } } } })();