Loading ClassHub...

JustCopy.ai Clone with JustCopy
`\n const win = window.open('', '_blank')\n win.document.write(printContent)\n win.document.close()\n win.print()\n }\n\n const isCoordinator = _optionalChain([userProfile, 'optionalAccess', _45 => _45.role, 'optionalAccess', _46 => _46.includes, 'call', _47 => _47('coordinator')])\n\n return (\n React.createElement('div', { className: \"p-8\",}\n , React.createElement('div', { className: \"flex items-center justify-between mb-6\" ,}\n , React.createElement('div', { className: \"flex items-center gap-4\" ,}\n , React.createElement('button', { onClick: onBack, className: \"p-2 hover:bg-gray-100 rounded-lg\" ,}\n , icons.ArrowLeft && React.createElement(icons.ArrowLeft, { className: \"w-5 h-5\" ,} )\n )\n , React.createElement('h1', { className: \"text-2xl font-bold text-gray-900\" ,}, data.name)\n )\n , React.createElement('button', { onClick: exportToPDF, className: \"px-4 py-2 bg-red-500 text-white rounded-lg flex items-center gap-2 hover:bg-red-600\" ,}\n , icons.Download && React.createElement(icons.Download, { className: \"w-4 h-4\" ,} ), \" Export PDF\"\n )\n )\n\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm border overflow-hidden\" ,}\n , React.createElement('div', { className: \"overflow-x-auto\",}\n , React.createElement('table', { className: \"w-full\",}\n , React.createElement('thead', { className: \"bg-gray-50\",}\n , React.createElement('tr', null\n , _optionalChain([data, 'access', _48 => _48.columns, 'optionalAccess', _49 => _49.map, 'call', _50 => _50((col, i) => (\n React.createElement('th', { key: i, className: \"px-4 py-3 text-left text-sm font-semibold text-gray-700 border-b\" ,}, col)\n ))])\n , isCoordinator && React.createElement('th', { className: \"px-4 py-3 border-b w-20\" ,}, \"Actions\")\n )\n )\n , React.createElement('tbody', null\n , _optionalChain([data, 'access', _51 => _51.rows, 'optionalAccess', _52 => _52.map, 'call', _53 => _53((row, i) => (\n React.createElement('tr', { key: row.id, className: \"hover:bg-gray-50\",}\n , _optionalChain([data, 'access', _54 => _54.columns, 'optionalAccess', _55 => _55.map, 'call', _56 => _56((col, j) => (\n React.createElement('td', { key: j, className: \"px-4 py-3 border-b text-gray-600\" ,}, row[col] || '-')\n ))])\n , isCoordinator && (\n React.createElement('td', { className: \"px-4 py-3 border-b\" ,}\n , React.createElement('button', { onClick: () => deleteRow(row.id), className: \"text-red-500 hover:text-red-700\" ,}\n , icons.Trash2 && React.createElement(icons.Trash2, { className: \"w-4 h-4\" ,} )\n )\n )\n )\n )\n ))])\n , isCoordinator && (\n React.createElement('tr', { className: \"bg-indigo-50\",}\n , _optionalChain([data, 'access', _57 => _57.columns, 'optionalAccess', _58 => _58.map, 'call', _59 => _59((col, i) => (\n React.createElement('td', { key: i, className: \"px-4 py-2 border-b\" ,}\n , React.createElement('input', { value: newRow[col] || '', onChange: (e) => setNewRow({ ...newRow, [col]: e.target.value }),\n placeholder: `Add ${col}`, className: \"w-full px-2 py-1 border rounded text-sm\" ,} )\n )\n ))])\n , React.createElement('td', { className: \"px-4 py-2 border-b\" ,}\n , React.createElement('button', { onClick: addRow, className: \"text-green-600 hover:text-green-700\" ,}\n , icons.Plus && React.createElement(icons.Plus, { className: \"w-5 h-5\" ,} )\n )\n )\n )\n )\n )\n )\n )\n )\n\n , isCoordinator && (\n React.createElement('div', { className: \"mt-4 flex gap-2\" ,}\n , React.createElement('input', { value: newColumn, onChange: (e) => setNewColumn(e.target.value), placeholder: \"New column name\" ,\n className: \"px-4 py-2 border rounded-lg\" ,} )\n , React.createElement('button', { onClick: addColumn, className: \"px-4 py-2 bg-indigo-600 text-white rounded-lg\" ,}, \"Add Column\" )\n )\n )\n )\n )\n}\n\nfunction SyllabusSection({ userProfile }) {\n const [files, setFiles] = useState([])\n const [uploading, setUploading] = useState(false)\n\n useEffect(() => { loadFiles() }, [])\n\n async function loadFiles() {\n const result = await window.db.query('syllabus', { sortBy: 'createdAt', sortOrder: 'desc' })\n setFiles(result.items || [])\n }\n\n async function handleUpload(e) {\n const file = e.target.files[0]\n if (!file) return\n setUploading(true)\n try {\n const result = await window.storage.upload(file)\n await window.db.create('syllabus', {\n name: file.name,\n url: result.url,\n type: file.type,\n size: file.size,\n uploadedBy: userProfile.name,\n uploadedById: userProfile.id,\n createdAt: new Date().toISOString()\n })\n loadFiles()\n } catch (err) {\n console.error('Upload failed:', err)\n }\n setUploading(false)\n }\n\n const isCoordinator = _optionalChain([userProfile, 'optionalAccess', _60 => _60.role, 'optionalAccess', _61 => _61.includes, 'call', _62 => _62('coordinator')])\n\n return (\n React.createElement('div', { className: \"p-8\",}\n , React.createElement('div', { className: \"flex items-center justify-between mb-8\" ,}\n , React.createElement('div', null\n , React.createElement('h1', { className: \"text-3xl font-bold text-gray-900\" ,}, \"📚 Syllabus & Materials\" )\n , React.createElement('p', { className: \"text-gray-500\",}, \"Access study materials and syllabus documents\" )\n )\n , isCoordinator && (\n React.createElement('label', { className: \"px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-500 text-white rounded-xl font-medium hover:shadow-lg transition-all flex items-center gap-2 cursor-pointer\" ,}\n , uploading ? React.createElement('div', { className: \"w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin\" ,}) : icons.Upload && React.createElement(icons.Upload, { className: \"w-5 h-5\" ,} )\n , uploading ? 'Uploading...' : 'Upload File'\n , React.createElement('input', { type: \"file\", onChange: handleUpload, className: \"hidden\", accept: \".pdf,.doc,.docx,.jpg,.jpeg,.png\",} )\n )\n )\n )\n\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\" ,}\n , files.map(file => (\n React.createElement('a', { key: file.id, href: file.url, target: \"_blank\", rel: \"noopener noreferrer\" ,\n className: \"bg-white rounded-xl p-6 shadow-sm border border-gray-100 hover:shadow-md hover:border-indigo-200 transition-all\" ,}\n , React.createElement('div', { className: `w-12 h-12 rounded-xl flex items-center justify-center mb-4 ${_optionalChain([file, 'access', _63 => _63.type, 'optionalAccess', _64 => _64.includes, 'call', _65 => _65('pdf')]) ? 'bg-red-100' : _optionalChain([file, 'access', _66 => _66.type, 'optionalAccess', _67 => _67.includes, 'call', _68 => _68('image')]) ? 'bg-blue-100' : 'bg-gray-100'}`,}\n , _optionalChain([file, 'access', _69 => _69.type, 'optionalAccess', _70 => _70.includes, 'call', _71 => _71('pdf')]) ? (\n React.createElement('span', { className: \"text-red-600 font-bold text-sm\" ,}, \"PDF\")\n ) : _optionalChain([file, 'access', _72 => _72.type, 'optionalAccess', _73 => _73.includes, 'call', _74 => _74('image')]) ? (\n icons.Image && React.createElement(icons.Image, { className: \"w-6 h-6 text-blue-600\" ,} )\n ) : (\n icons.FileText && React.createElement(icons.FileText, { className: \"w-6 h-6 text-gray-600\" ,} )\n )\n )\n , React.createElement('h3', { className: \"font-bold text-gray-900 mb-1 truncate\" ,}, file.name)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"By \" , file.uploadedBy)\n , React.createElement('p', { className: \"text-xs text-gray-400 mt-2\" ,}, (file.size / 1024).toFixed(1), \" KB\" )\n )\n ))\n , files.length === 0 && (\n React.createElement('div', { className: \"col-span-full text-center py-12 text-gray-500\" ,}\n , React.createElement('p', null, \"No syllabus files uploaded yet.\" )\n )\n )\n )\n )\n )\n}\n\nfunction MembersSection({ userProfile }) {\n const [members, setMembers] = useState([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => { loadMembers() }, [])\n\n async function loadMembers() {\n setLoading(true)\n const result = await window.db.query('profiles', { sortBy: 'createdAt', sortOrder: 'desc' })\n setMembers(result.items || [])\n setLoading(false)\n }\n\n async function toggleBlock(member) {\n await window.db.update('profiles', member.id, { isBlocked: !member.isBlocked })\n loadMembers()\n }\n\n const isCoordinator = _optionalChain([userProfile, 'optionalAccess', _75 => _75.role, 'optionalAccess', _76 => _76.includes, 'call', _77 => _77('coordinator')])\n const coordinators = members.filter(m => _optionalChain([m, 'access', _78 => _78.role, 'optionalAccess', _79 => _79.includes, 'call', _80 => _80('coordinator')]))\n const students = members.filter(m => !_optionalChain([m, 'access', _81 => _81.role, 'optionalAccess', _82 => _82.includes, 'call', _83 => _83('coordinator')]))\n\n return (\n React.createElement('div', { className: \"p-8\",}\n , React.createElement('div', { className: \"mb-8\",}\n , React.createElement('h1', { className: \"text-3xl font-bold text-gray-900\" ,}, \"👥 All Members\" )\n , React.createElement('p', { className: \"text-gray-500\",}, members.length, \" members in this class\" )\n )\n\n , React.createElement('div', { className: \"mb-8\",}\n , React.createElement('h2', { className: \"text-xl font-bold text-gray-800 mb-4\" ,}, \"Class Representatives\" )\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 gap-4\" ,}\n , coordinators.map(coord => (\n React.createElement('div', { key: coord.id, className: `bg-gradient-to-r ${coord.gender === 'male' ? 'from-blue-500 to-cyan-500' : 'from-pink-500 to-rose-500'} rounded-xl p-6 text-white`,}\n , React.createElement('div', { className: \"flex items-center gap-4\" ,}\n , React.createElement('div', { className: \"w-16 h-16 rounded-full bg-white/20 flex items-center justify-center text-2xl font-bold\" ,}\n , _optionalChain([coord, 'access', _84 => _84.name, 'optionalAccess', _85 => _85.charAt, 'call', _86 => _86(0)])\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"text-white/80 text-sm\" ,}, coord.gender === 'male' ? 'Boys' : 'Girls', \" Coordinator\" )\n , React.createElement('h3', { className: \"text-xl font-bold\" ,}, coord.name)\n , React.createElement('p', { className: \"text-white/80 text-sm\" ,}, coord.email)\n )\n )\n )\n ))\n )\n )\n\n , React.createElement('h2', { className: \"text-xl font-bold text-gray-800 mb-4\" ,}, \"Students\")\n , loading ? (\n React.createElement('div', { className: \"text-center py-12\" ,}\n , React.createElement('div', { className: \"w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto\" ,})\n )\n ) : (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm border overflow-hidden\" ,}\n , React.createElement('table', { className: \"w-full\",}\n , React.createElement('thead', { className: \"bg-gray-50\",}\n , React.createElement('tr', null\n , React.createElement('th', { className: \"px-6 py-4 text-left text-sm font-semibold text-gray-700\" ,}, \"Member\")\n , React.createElement('th', { className: \"px-6 py-4 text-left text-sm font-semibold text-gray-700\" ,}, \"Gender\")\n , React.createElement('th', { className: \"px-6 py-4 text-left text-sm font-semibold text-gray-700\" ,}, \"Status\")\n , isCoordinator && React.createElement('th', { className: \"px-6 py-4 text-left text-sm font-semibold text-gray-700\" ,}, \"Actions\")\n )\n )\n , React.createElement('tbody', null\n , students.map(member => (\n React.createElement('tr', { key: member.id, className: \"hover:bg-gray-50 border-t\" ,}\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: `w-10 h-10 rounded-full bg-gradient-to-r ${member.gender === 'male' ? 'from-blue-400 to-cyan-400' : 'from-pink-400 to-rose-400'} flex items-center justify-center text-white font-bold`,}\n , _optionalChain([member, 'access', _87 => _87.name, 'optionalAccess', _88 => _88.charAt, 'call', _89 => _89(0)])\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, member.name)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, member.email)\n )\n )\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('span', { className: `px-3 py-1 rounded-full text-sm ${member.gender === 'male' ? 'bg-blue-100 text-blue-700' : 'bg-pink-100 text-pink-700'}`,}\n , member.gender === 'male' ? '👨 Male' : '👩 Female'\n )\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , member.isBlocked ? (\n React.createElement('span', { className: \"px-3 py-1 bg-red-100 text-red-700 rounded-full text-sm\" ,}, \"Blocked\")\n ) : (\n React.createElement('span', { className: \"px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm\" ,}, \"Active\")\n )\n )\n , isCoordinator && (\n React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('button', { onClick: () => toggleBlock(member),\n className: `px-4 py-2 rounded-lg text-sm font-medium ${member.isBlocked ? 'bg-green-100 text-green-700 hover:bg-green-200' : 'bg-red-100 text-red-700 hover:bg-red-200'}`,}\n , member.isBlocked ? 'Unblock' : 'Block'\n )\n )\n )\n )\n ))\n )\n )\n )\n )\n )\n )\n}"; // 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 'ff0ed31c-b69c-4728-9c3d-8d671a5d9540'; }; // 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) { oauthRedirectUrl = window.location.origin + '/'; } else if (hostname === 'apps.justcopy.ai') { // apps.justcopy.ai is registered directly in Cognito callback URLs oauthRedirectUrl = 'https://apps.justcopy.ai/'; } else { // Other domains proxy through oauth.justcopy.link 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); // Save current path before OAuth redirect (to restore after login) if (window.location.pathname.length > 1 && !new URLSearchParams(window.location.search).has('code')) { sessionStorage.setItem('justcopy_auth_return', window.location.pathname); } // Check if we're on an OAuth callback (URL has code parameter) var urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code')) { console.log('[JustCopy Auth] OAuth callback detected, Amplify will handle token exchange'); // Redirect back to the original project after auth completes var returnPath = sessionStorage.getItem('justcopy_auth_return'); if (returnPath && window.location.pathname === '/') { sessionStorage.removeItem('justcopy_auth_return'); setTimeout(function() { window.location.href = returnPath; }, 1500); } } // 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': 'ff0ed31c-b69c-4728-9c3d-8d671a5d9540' }, 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', 'ff0ed31c-b69c-4728-9c3d-8d671a5d9540'); 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 + '/' : hostname === 'apps.justcopy.ai' ? 'https://apps.justcopy.ai/' : '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/ff0ed31c-b69c-4728-9c3d-8d671a5d9540'; } 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: 'ff0ed31c-b69c-4728-9c3d-8d671a5d9540' }); 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() { // Save current path so we can redirect back after Cognito logout try { sessionStorage.setItem('justcopy_logout_return', window.location.pathname); } catch(e) {} 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 inline-block' }, 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 left-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 }, '*'); }); } } } })();