نظام الحضور بالمواقع الجغرافية

Geolocation Attendance System

المميزات الرئيسية

تتبع GPS دقيق

تحديد الموقع بدقة عالية باستخدام GPS

التحقق الجغرافي

تسجيل الحضور فقط داخل المناطق المصرح بها

تقارير شاملة

تقارير يومية وأسبوعية وشهرية مفصلة

نظام آمن ومحمي للتتبع الجغرافي للحضور والانصراف

JustCopy.ai Clone with JustCopy
\n \n `\n\n const printWindow = window.open('', '_blank')\n printWindow.document.write(printContent)\n printWindow.document.close()\n printWindow.print()\n }\n\n if (loading) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-8\" ,}\n , React.createElement('div', { className: \"animate-pulse space-y-4\" ,}\n , React.createElement('div', { className: \"h-8 bg-gray-200 rounded w-1/4\" ,})\n , React.createElement('div', { className: \"grid grid-cols-4 gap-4\" ,}\n , [1, 2, 3, 4].map(i => (\n React.createElement('div', { key: i, className: \"h-24 bg-gray-200 rounded-lg\" ,})\n ))\n )\n )\n )\n )\n }\n\n return (\n React.createElement('div', { className: \"space-y-6\",}\n , React.createElement('div', { className: \"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4\" ,}\n , React.createElement('h2', { className: \"text-2xl font-bold text-gray-900\" ,}, \"التقارير\")\n , React.createElement('div', { className: \"flex gap-2\" ,}\n , React.createElement('button', {\n onClick: exportToExcel,\n className: \"flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-5 h-5\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" ,} )\n ), \"Excel\"\n\n )\n , React.createElement('button', {\n onClick: exportToPDF,\n className: \"flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-5 h-5\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z\" ,} )\n ), \"PDF\"\n\n )\n )\n )\n\n /* Stats Cards */\n , React.createElement('div', { className: \"grid grid-cols-2 md:grid-cols-5 gap-4\" ,}\n , React.createElement(StatCard, { title: \"إجمالي السجلات\" , value: stats.total, color: \"blue\",} )\n , React.createElement(StatCard, { title: \"حاضرين\", value: stats.present, color: \"green\",} )\n , React.createElement(StatCard, { title: \"غائبين\", value: stats.absent, color: \"red\",} )\n , React.createElement(StatCard, { title: \"متأخرين\", value: stats.late, color: \"yellow\",} )\n , React.createElement(StatCard, { title: \"إجمالي الساعات\" , value: stats.totalHours.toFixed(1), color: \"purple\", suffix: \"ساعة\",} )\n )\n\n /* Filters */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('h3', { className: \"font-semibold text-gray-900 mb-4\" ,}, \"تصفية النتائج\" )\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4\" ,}\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"من تاريخ\" )\n , React.createElement('input', {\n type: \"date\",\n value: filters.dateFrom,\n onChange: (e) => setFilters({...filters, dateFrom: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"إلى تاريخ\" )\n , React.createElement('input', {\n type: \"date\",\n value: filters.dateTo,\n onChange: (e) => setFilters({...filters, dateTo: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الموظف\")\n , React.createElement('select', {\n value: filters.employeeId,\n onChange: (e) => setFilters({...filters, employeeId: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n\n , React.createElement('option', { value: \"\",}, \"الكل\")\n , employees.map(emp => (\n React.createElement('option', { key: emp.id, value: emp.id,}, emp.name)\n ))\n )\n )\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الموقع\")\n , React.createElement('select', {\n value: filters.locationId,\n onChange: (e) => setFilters({...filters, locationId: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n\n , React.createElement('option', { value: \"\",}, \"الكل\")\n , locations.map(loc => (\n React.createElement('option', { key: loc.id, value: loc.id,}, loc.name)\n ))\n )\n )\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الحالة\")\n , React.createElement('select', {\n value: filters.status,\n onChange: (e) => setFilters({...filters, status: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n\n , React.createElement('option', { value: \"\",}, \"الكل\")\n , React.createElement('option', { value: \"present\",}, \"حاضر\")\n , React.createElement('option', { value: \"absent\",}, \"غائب\")\n )\n )\n )\n )\n\n /* Attendance Table */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm 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 , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"التاريخ\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الموظف\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الموقع\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الدخول\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الخروج\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الحالة\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الساعات\")\n )\n )\n , React.createElement('tbody', { className: \"divide-y divide-gray-200\" ,}\n , filteredAttendance.length === 0 ? (\n React.createElement('tr', null\n , React.createElement('td', { colSpan: \"7\", className: \"px-6 py-12 text-center text-gray-500\" ,}, \"لا توجد سجلات مطابقة للفلاتر\"\n\n )\n )\n ) : (\n filteredAttendance.map((record) => (\n React.createElement('tr', { key: record.id, className: \"hover:bg-gray-50\",}\n , React.createElement('td', { className: \"px-6 py-4 text-gray-900\" ,}\n , new Date(record.date).toLocaleDateString('ar-SA')\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('div', { className: \"font-medium text-gray-900\" ,}, record.employeeName || '-')\n )\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}, record.locationName || '-')\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}\n , record.checkInTime ? new Date(record.checkInTime).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' }) : '-'\n )\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}\n , record.checkOutTime ? new Date(record.checkOutTime).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' }) : '-'\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('span', { className: `px-2 py-1 text-xs font-medium rounded-full ${\n record.status === 'present' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'\n }`,}\n , record.status === 'present' ? 'حاضر' : 'غائب'\n )\n , record.isLate && (\n React.createElement('span', { className: \"mr-2 px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-700\" ,}, \"متأخر\"\n\n )\n )\n )\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}\n , record.workHours ? record.workHours.toFixed(2) + ' ساعة' : '-'\n )\n )\n ))\n )\n )\n )\n )\n )\n )\n )\n}\n\nfunction StatCard({ title, value, color, suffix = '' }) {\n const colors = {\n blue: 'bg-blue-500',\n green: 'bg-green-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500',\n purple: 'bg-purple-500'\n }\n\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-4\" ,}\n , React.createElement('div', { className: \"flex items-center justify-between mb-2\" ,}\n , React.createElement('div', { className: `w-10 h-10 ${colors[color]} rounded-lg flex items-center justify-center`,}\n , React.createElement('svg', { className: \"w-5 h-5 text-white\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\" ,} )\n )\n )\n )\n , React.createElement('p', { className: \"text-2xl font-bold text-gray-900\" ,}, value, suffix)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, title)\n )\n )\n}\n\n// Component: OverviewView\nfunction OverviewView({ user }) {\n const [stats, setStats] = useState({\n totalEmployees: 0,\n totalLocations: 0,\n todayAttendance: 0,\n presentToday: 0,\n absentToday: 0,\n lateToday: 0\n })\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n loadStats()\n }, [])\n\n const loadStats = async () => {\n try {\n const [employeesResult, locationsResult, attendanceResult] = await Promise.all([\n window.db.query('employees'),\n window.db.query('locations'),\n window.db.query('attendance')\n ])\n\n const today = new Date().toISOString().split('T')[0]\n const todayRecords = _optionalChain([attendanceResult, 'access', _4 => _4.items, 'optionalAccess', _5 => _5.filter, 'call', _6 => _6(r => r.date === today)]) || []\n\n setStats({\n totalEmployees: employeesResult.count || 0,\n totalLocations: locationsResult.count || 0,\n todayAttendance: todayRecords.length,\n presentToday: todayRecords.filter(r => r.status === 'present').length,\n absentToday: todayRecords.filter(r => r.status === 'absent').length,\n lateToday: todayRecords.filter(r => r.isLate).length\n })\n } catch (err) {\n console.error('Error loading stats:', err)\n } finally {\n setLoading(false)\n }\n }\n\n if (loading) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-8\" ,}\n , React.createElement('div', { className: \"animate-pulse space-y-4\" ,}\n , React.createElement('div', { className: \"h-4 bg-gray-200 rounded w-1/4\" ,})\n , React.createElement('div', { className: \"grid grid-cols-2 md:grid-cols-4 gap-4\" ,}\n , [1, 2, 3, 4].map(i => (\n React.createElement('div', { key: i, className: \"h-24 bg-gray-200 rounded-lg\" ,})\n ))\n )\n )\n )\n )\n }\n\n return (\n React.createElement('div', { className: \"space-y-6\",}\n , React.createElement('div', { className: \"flex items-center justify-between\" ,}\n , React.createElement('h2', { className: \"text-2xl font-bold text-gray-900\" ,}, \"نظرة عامة\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, new Date().toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }))\n )\n\n /* Stats Grid */\n , React.createElement('div', { className: \"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4\" ,}\n , React.createElement(StatCard, {\n title: \"إجمالي الموظفين\" ,\n value: stats.totalEmployees,\n icon: \"users\",\n color: \"blue\",}\n )\n , React.createElement(StatCard, {\n title: \"المواقع\",\n value: stats.totalLocations,\n icon: \"map-pin\",\n color: \"green\",}\n )\n , React.createElement(StatCard, {\n title: \"حضور اليوم\" ,\n value: stats.todayAttendance,\n icon: \"check-circle\",\n color: \"purple\",}\n )\n , React.createElement(StatCard, {\n title: \"حاضرين\",\n value: stats.presentToday,\n icon: \"user-check\",\n color: \"emerald\",}\n )\n , React.createElement(StatCard, {\n title: \"غائبين\",\n value: stats.absentToday,\n icon: \"user-x\",\n color: \"red\",}\n )\n , React.createElement(StatCard, {\n title: \"متأخرين\",\n value: stats.lateToday,\n icon: \"clock\",\n color: \"yellow\",}\n )\n )\n\n /* Quick Actions */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('h3', { className: \"text-lg font-semibold text-gray-900 mb-4\" ,}, \"إجراءات سريعة\" )\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-3 gap-4\" ,}\n , React.createElement('button', {\n onClick: () => window.location.href = '/dashboard?view=employees',\n className: \"flex items-center gap-3 p-4 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors\" ,}\n\n , React.createElement('div', { className: \"w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-5 h-5 text-white\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 6v6m0 0v6m0-6h6m-6 0H6\" ,} )\n )\n )\n , React.createElement('div', { className: \"text-right\",}\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, \"إضافة موظف\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"تسجيل موظف جديد\" )\n )\n )\n\n , React.createElement('button', {\n onClick: () => window.location.href = '/dashboard?view=locations',\n className: \"flex items-center gap-3 p-4 bg-green-50 rounded-lg hover:bg-green-100 transition-colors\" ,}\n\n , React.createElement('div', { className: \"w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-5 h-5 text-white\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 6v6m0 0v6m0-6h6m-6 0H6\" ,} )\n )\n )\n , React.createElement('div', { className: \"text-right\",}\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, \"إضافة موقع\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"تحديد موقع عمل جديد\" )\n )\n )\n\n , React.createElement('button', {\n onClick: () => window.location.href = '/dashboard?view=reports',\n className: \"flex items-center gap-3 p-4 bg-purple-50 rounded-lg hover:bg-purple-100 transition-colors\" ,}\n\n , React.createElement('div', { className: \"w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-5 h-5 text-white\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" ,} )\n )\n )\n , React.createElement('div', { className: \"text-right\",}\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, \"تصدير تقرير\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"Excel أو PDF\" )\n )\n )\n )\n )\n\n /* Recent Activity */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('h3', { className: \"text-lg font-semibold text-gray-900 mb-4\" ,}, \"النشاط الأخير\" )\n , React.createElement(RecentActivityList, null )\n )\n )\n )\n}\n\nfunction StatCard({ title, value, icon, color }) {\n const colors = {\n blue: 'bg-blue-500',\n green: 'bg-green-500',\n purple: 'bg-purple-500',\n emerald: 'bg-emerald-500',\n red: 'bg-red-500',\n yellow: 'bg-yellow-500'\n }\n\n const icons = {\n users: React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z\" ,} ),\n 'map-pin': React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} ),\n 'check-circle': React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" ,} ),\n 'user-check': React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z\" ,} ),\n 'user-x': React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z\" ,} ),\n clock: React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" ,} )\n }\n\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-4\" ,}\n , React.createElement('div', { className: \"flex items-center justify-between mb-2\" ,}\n , React.createElement('div', { className: `w-10 h-10 ${colors[color]} rounded-lg flex items-center justify-center`,}\n , React.createElement('svg', { className: \"w-5 h-5 text-white\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , icons[icon]\n )\n )\n )\n , React.createElement('p', { className: \"text-2xl font-bold text-gray-900\" ,}, value)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, title)\n )\n )\n}\n\nfunction RecentActivityList() {\n const [activities, setActivities] = useState([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n loadActivities()\n }, [])\n\n const loadActivities = async () => {\n try {\n const result = await window.db.query('attendance', { limit: 10, sort: 'createdAt', order: 'desc' })\n setActivities(result.items || [])\n } catch (err) {\n console.error('Error loading activities:', err)\n } finally {\n setLoading(false)\n }\n }\n\n if (loading) {\n return React.createElement('div', { className: \"text-center py-8 text-gray-500\" ,}, \"جاري التحميل...\" )\n }\n\n if (activities.length === 0) {\n return React.createElement('div', { className: \"text-center py-8 text-gray-500\" ,}, \"لا يوجد نشاط حديث\" )\n }\n\n return (\n React.createElement('div', { className: \"space-y-3\",}\n , activities.map((activity) => (\n React.createElement('div', { key: activity.id, className: \"flex items-center justify-between p-3 bg-gray-50 rounded-lg\" ,}\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: `w-8 h-8 rounded-full flex items-center justify-center ${\n activity.type === 'check-in' ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'\n }`,}\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , activity.type === 'check-in' ? (\n React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1\" ,} )\n ) : (\n React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1\" ,} )\n )\n )\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, activity.employeeName || 'موظف')\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, activity.locationName || 'غير محدد')\n )\n )\n , React.createElement('div', { className: \"text-left\",}\n , React.createElement('p', { className: \"text-sm font-medium text-gray-900\" ,}\n , activity.type === 'check-in' ? 'دخول' : 'خروج'\n )\n , React.createElement('p', { className: \"text-xs text-gray-500\" ,}\n , new Date(activity.createdAt).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' })\n )\n )\n )\n ))\n )\n )\n}\n\n// Component: LocationsView\nfunction LocationsView({ user }) {\n const [locations, setLocations] = useState([])\n const [loading, setLoading] = useState(true)\n const [showModal, setShowModal] = useState(false)\n const [editingLocation, setEditingLocation] = useState(null)\n const [currentLocation, setCurrentLocation] = useState(null)\n\n useEffect(() => {\n loadLocations()\n }, [])\n\n const loadLocations = async () => {\n try {\n const result = await window.db.query('locations')\n setLocations(result.items || [])\n } catch (err) {\n console.error('Error loading locations:', err)\n } finally {\n setLoading(false)\n }\n }\n\n const handleAddLocation = () => {\n setEditingLocation(null)\n setShowModal(true)\n }\n\n const handleEditLocation = (location) => {\n setEditingLocation(location)\n setShowModal(true)\n }\n\n const handleDeleteLocation = async (id) => {\n if (!confirm('هل أنت متأكد من حذف هذا الموقع؟')) return\n\n try {\n await window.db.delete('locations', id)\n setLocations(locations.filter(l => l.id !== id))\n } catch (err) {\n alert('فشل حذف الموقع')\n }\n }\n\n const handleGetCurrentLocation = () => {\n if (!navigator.geolocation) {\n alert('المتصفح لا يدعم تحديد الموقع')\n return\n }\n\n navigator.geolocation.getCurrentPosition(\n (position) => {\n setCurrentLocation({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracy: position.coords.accuracy\n })\n },\n (error) => {\n let message = 'فشل تحديد الموقع'\n switch (error.code) {\n case error.PERMISSION_DENIED:\n message = 'تم رفض إذن تحديد الموقع'\n break\n case error.POSITION_UNAVAILABLE:\n message = 'معلومات الموقع غير متاحة'\n break\n case error.TIMEOUT:\n message = 'انتهت مهلة تحديد الموقع'\n break\n }\n alert(message)\n },\n {\n enableHighAccuracy: true,\n timeout: 10000,\n maximumAge: 0\n }\n )\n }\n\n if (loading) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-8\" ,}\n , React.createElement('div', { className: \"animate-pulse space-y-4\" ,}\n , React.createElement('div', { className: \"h-8 bg-gray-200 rounded w-1/4\" ,})\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 gap-4\" ,}\n , [1, 2, 3, 4].map(i => (\n React.createElement('div', { key: i, className: \"h-32 bg-gray-200 rounded-lg\" ,})\n ))\n )\n )\n )\n )\n }\n\n return (\n React.createElement('div', { className: \"space-y-6\",}\n , React.createElement('div', { className: \"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4\" ,}\n , React.createElement('h2', { className: \"text-2xl font-bold text-gray-900\" ,}, \"إدارة المواقع\" )\n , React.createElement('button', {\n onClick: handleAddLocation,\n className: \"flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-5 h-5\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 6v6m0 0v6m0-6h6m-6 0H6\" ,} )\n ), \"إضافة موقع\"\n\n )\n )\n\n /* Current Location */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('div', { className: \"flex items-center justify-between mb-4\" ,}\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: \"w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-5 h-5 text-blue-600\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n )\n )\n , React.createElement('div', null\n , React.createElement('h3', { className: \"font-semibold text-gray-900\" ,}, \"موقعك الحالي\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"استخدم هذا الموقع لإضافة موقع عمل جديد\" )\n )\n )\n , React.createElement('button', {\n onClick: handleGetCurrentLocation,\n className: \"px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm\" ,}\n, \"تحديد موقعي\"\n\n )\n )\n\n , currentLocation && (\n React.createElement('div', { className: \"bg-blue-50 rounded-lg p-4\" ,}\n , React.createElement('div', { className: \"grid grid-cols-2 md:grid-cols-3 gap-4 text-sm\" ,}\n , React.createElement('div', null\n , React.createElement('p', { className: \"text-gray-500 mb-1\" ,}, \"خط العرض\" )\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, currentLocation.latitude.toFixed(6))\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"text-gray-500 mb-1\" ,}, \"خط الطول\" )\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, currentLocation.longitude.toFixed(6))\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"text-gray-500 mb-1\" ,}, \"الدقة\")\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, \"±\", currentLocation.accuracy.toFixed(0), \" متر\" )\n )\n )\n )\n )\n )\n\n /* Locations Grid */\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 gap-4\" ,}\n , locations.length === 0 ? (\n React.createElement('div', { className: \"col-span-full bg-white rounded-xl shadow-sm p-12 text-center\" ,}\n , React.createElement('svg', { className: \"w-16 h-16 text-gray-300 mx-auto mb-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n )\n , React.createElement('p', { className: \"text-gray-500 mb-2\" ,}, \"لا يوجد مواقع\" )\n , React.createElement('button', {\n onClick: handleAddLocation,\n className: \"text-blue-600 hover:text-blue-700 font-medium\" ,}\n, \"إضافة موقع جديد\"\n\n )\n )\n ) : (\n locations.map((location) => (\n React.createElement(LocationCard, {\n key: location.id,\n location: location,\n onEdit: () => handleEditLocation(location),\n onDelete: () => handleDeleteLocation(location.id),}\n )\n ))\n )\n )\n\n /* Modal */\n , showModal && (\n React.createElement(LocationModal, {\n location: editingLocation,\n currentLocation: currentLocation,\n onClose: () => setShowModal(false),\n onSave: loadLocations,}\n )\n )\n )\n )\n}\n\nfunction LocationCard({ location, onEdit, onDelete }) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6 hover:shadow-md transition-shadow\" ,}\n , React.createElement('div', { className: \"flex items-start justify-between mb-4\" ,}\n , React.createElement('div', { className: \"flex items-center gap-3\" ,}\n , React.createElement('div', { className: \"w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-6 h-6 text-green-600\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n )\n )\n , React.createElement('div', null\n , React.createElement('h3', { className: \"font-semibold text-gray-900\" ,}, location.name)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, location.address || 'لا يوجد عنوان')\n )\n )\n , React.createElement('div', { className: \"flex items-center gap-1\" ,}\n , React.createElement('button', {\n onClick: onEdit,\n className: \"p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z\" ,} )\n )\n )\n , React.createElement('button', {\n onClick: onDelete,\n className: \"p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" ,} )\n )\n )\n )\n )\n\n , React.createElement('div', { className: \"space-y-2 text-sm\" ,}\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"خط العرض:\" )\n , React.createElement('span', { className: \"font-medium text-gray-900\" ,}, location.latitude.toFixed(6))\n )\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"خط الطول:\" )\n , React.createElement('span', { className: \"font-medium text-gray-900\" ,}, location.longitude.toFixed(6))\n )\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"نطاق التحقق:\" )\n , React.createElement('span', { className: \"font-medium text-gray-900\" ,}, location.radius, \" متر\" )\n )\n )\n\n , React.createElement('div', { className: \"mt-4 pt-4 border-t border-gray-100\" ,}\n , React.createElement('a', {\n href: `https://www.google.com/maps?q=${location.latitude},${location.longitude}`,\n target: \"_blank\",\n rel: \"noopener noreferrer\" ,\n className: \"flex items-center justify-center gap-2 text-blue-600 hover:text-blue-700 text-sm font-medium\" ,}\n\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\" ,} )\n ), \"عرض على الخريطة\"\n\n )\n )\n )\n )\n}\n\nfunction LocationModal({ location, currentLocation, onClose, onSave }) {\n const [formData, setFormData] = useState({\n name: '',\n address: '',\n latitude: '',\n longitude: '',\n radius: 100\n })\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n useEffect(() => {\n if (location) {\n setFormData({\n name: location.name || '',\n address: location.address || '',\n latitude: _optionalChain([location, 'access', _7 => _7.latitude, 'optionalAccess', _8 => _8.toString, 'call', _9 => _9()]) || '',\n longitude: _optionalChain([location, 'access', _10 => _10.longitude, 'optionalAccess', _11 => _11.toString, 'call', _12 => _12()]) || '',\n radius: location.radius || 100\n })\n } else if (currentLocation) {\n setFormData(prev => ({\n ...prev,\n latitude: currentLocation.latitude.toString(),\n longitude: currentLocation.longitude.toString()\n }))\n }\n }, [location, currentLocation])\n\n const handleSubmit = async (e) => {\n e.preventDefault()\n setLoading(true)\n setError('')\n\n try {\n const data = {\n ...formData,\n latitude: parseFloat(formData.latitude),\n longitude: parseFloat(formData.longitude),\n radius: parseInt(formData.radius)\n }\n\n if (location) {\n await window.db.update('locations', location.id, data)\n } else {\n await window.db.create('locations', {\n ...data,\n createdAt: new Date().toISOString()\n })\n }\n onSave()\n onClose()\n } catch (err) {\n setError('فشل حفظ البيانات')\n } finally {\n setLoading(false)\n }\n }\n\n return (\n React.createElement('div', { className: \"fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50\" ,}\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto\" ,}\n , React.createElement('div', { className: \"p-6 border-b border-gray-200\" ,}\n , React.createElement('h3', { className: \"text-xl font-bold text-gray-900\" ,}\n , location ? 'تعديل الموقع' : 'إضافة موقع جديد'\n )\n )\n\n , React.createElement('form', { onSubmit: handleSubmit, className: \"p-6 space-y-4\" ,}\n , error && (\n React.createElement('div', { className: \"p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm\" ,}\n , error\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"اسم الموقع\" )\n , React.createElement('input', {\n type: \"text\",\n required: true,\n value: formData.name,\n onChange: (e) => setFormData({...formData, name: e.target.value}),\n placeholder: \"مثال: المكتب الرئيسي\" ,\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"العنوان\")\n , React.createElement('input', {\n type: \"text\",\n value: formData.address,\n onChange: (e) => setFormData({...formData, address: e.target.value}),\n placeholder: \"العنوان الكامل\" ,\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n\n , React.createElement('div', { className: \"grid grid-cols-2 gap-4\" ,}\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"خط العرض\" )\n , React.createElement('input', {\n type: \"number\",\n step: \"any\",\n required: true,\n value: formData.latitude,\n onChange: (e) => setFormData({...formData, latitude: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"خط الطول\" )\n , React.createElement('input', {\n type: \"number\",\n step: \"any\",\n required: true,\n value: formData.longitude,\n onChange: (e) => setFormData({...formData, longitude: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"نطاق التحقق (متر)\"\n\n )\n , React.createElement('input', {\n type: \"number\",\n min: \"10\",\n max: \"1000\",\n required: true,\n value: formData.radius,\n onChange: (e) => setFormData({...formData, radius: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n , React.createElement('p', { className: \"text-xs text-gray-500 mt-1\" ,}, \"الموظف يمكنه تسجيل الحضور ضمن هذا النطاق من الموقع\"\n\n )\n )\n\n , React.createElement('div', { className: \"flex gap-3 pt-4\" ,}\n , React.createElement('button', {\n type: \"button\",\n onClick: onClose,\n className: \"flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors\" ,}\n, \"إلغاء\"\n\n )\n , React.createElement('button', {\n type: \"submit\",\n disabled: loading,\n className: \"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50\" ,}\n\n , loading ? 'جاري الحفظ...' : 'حفظ'\n )\n )\n )\n )\n )\n )\n}\n\n// Component: EmployeesView\nfunction EmployeesView({ user }) {\n const [employees, setEmployees] = useState([])\n const [locations, setLocations] = useState([])\n const [loading, setLoading] = useState(true)\n const [showModal, setShowModal] = useState(false)\n const [editingEmployee, setEditingEmployee] = useState(null)\n const [searchTerm, setSearchTerm] = useState('')\n\n useEffect(() => {\n loadData()\n }, [])\n\n const loadData = async () => {\n try {\n const [employeesResult, locationsResult] = await Promise.all([\n window.db.query('employees'),\n window.db.query('locations')\n ])\n setEmployees(employeesResult.items || [])\n setLocations(locationsResult.items || [])\n } catch (err) {\n console.error('Error loading data:', err)\n } finally {\n setLoading(false)\n }\n }\n\n const handleAddEmployee = () => {\n setEditingEmployee(null)\n setShowModal(true)\n }\n\n const handleEditEmployee = (employee) => {\n setEditingEmployee(employee)\n setShowModal(true)\n }\n\n const handleDeleteEmployee = async (id) => {\n if (!confirm('هل أنت متأكد من حذف هذا الموظف؟')) return\n\n try {\n await window.db.delete('employees', id)\n setEmployees(employees.filter(e => e.id !== id))\n } catch (err) {\n alert('فشل حذف الموظف')\n }\n }\n\n const filteredEmployees = employees.filter(emp =>\n _optionalChain([emp, 'access', _13 => _13.name, 'optionalAccess', _14 => _14.toLowerCase, 'call', _15 => _15(), 'access', _16 => _16.includes, 'call', _17 => _17(searchTerm.toLowerCase())]) ||\n _optionalChain([emp, 'access', _18 => _18.email, 'optionalAccess', _19 => _19.toLowerCase, 'call', _20 => _20(), 'access', _21 => _21.includes, 'call', _22 => _22(searchTerm.toLowerCase())]) ||\n _optionalChain([emp, 'access', _23 => _23.phone, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25(searchTerm)])\n )\n\n if (loading) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-8\" ,}\n , React.createElement('div', { className: \"animate-pulse space-y-4\" ,}\n , React.createElement('div', { className: \"h-8 bg-gray-200 rounded w-1/4\" ,})\n , React.createElement('div', { className: \"space-y-3\",}\n , [1, 2, 3, 4, 5].map(i => (\n React.createElement('div', { key: i, className: \"h-16 bg-gray-200 rounded-lg\" ,})\n ))\n )\n )\n )\n )\n }\n\n return (\n React.createElement('div', { className: \"space-y-6\",}\n , React.createElement('div', { className: \"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4\" ,}\n , React.createElement('h2', { className: \"text-2xl font-bold text-gray-900\" ,}, \"إدارة الموظفين\" )\n , React.createElement('button', {\n onClick: handleAddEmployee,\n className: \"flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-5 h-5\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 6v6m0 0v6m0-6h6m-6 0H6\" ,} )\n ), \"إضافة موظف\"\n\n )\n )\n\n /* Search */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-4\" ,}\n , React.createElement('div', { className: \"relative\",}\n , React.createElement('svg', { className: \"absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\" ,} )\n )\n , React.createElement('input', {\n type: \"text\",\n placeholder: \"بحث بالاسم، البريد الإلكتروني، أو الهاتف...\" ,\n value: searchTerm,\n onChange: (e) => setSearchTerm(e.target.value),\n className: \"w-full pr-10 pl-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n )\n\n /* Employees List */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm overflow-hidden\" ,}\n , filteredEmployees.length === 0 ? (\n React.createElement('div', { className: \"text-center py-12\" ,}\n , React.createElement('svg', { className: \"w-16 h-16 text-gray-300 mx-auto mb-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z\" ,} )\n )\n , React.createElement('p', { className: \"text-gray-500 mb-2\" ,}, \"لا يوجد موظفين\" )\n , React.createElement('button', {\n onClick: handleAddEmployee,\n className: \"text-blue-600 hover:text-blue-700 font-medium\" ,}\n, \"إضافة موظف جديد\"\n\n )\n )\n ) : (\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 , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الموظف\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الدور\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الموقع\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الهاتف\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"الحالة\")\n , React.createElement('th', { className: \"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\" ,}, \"إجراءات\")\n )\n )\n , React.createElement('tbody', { className: \"divide-y divide-gray-200\" ,}\n , filteredEmployees.map((employee) => (\n React.createElement('tr', { key: employee.id, className: \"hover:bg-gray-50\",}\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 bg-blue-100 rounded-full flex items-center justify-center\" ,}\n , React.createElement('span', { className: \"text-blue-600 font-medium\" ,}\n , _optionalChain([employee, 'access', _26 => _26.name, 'optionalAccess', _27 => _27.charAt, 'call', _28 => _28(0)]) || '?'\n )\n )\n , React.createElement('div', null\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}, employee.name)\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, employee.email)\n )\n )\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('span', { className: `px-2 py-1 text-xs font-medium rounded-full ${\n employee.role === 'admin' ? 'bg-purple-100 text-purple-700' :\n employee.role === 'supervisor' ? 'bg-blue-100 text-blue-700' :\n 'bg-gray-100 text-gray-700'\n }`,}\n , employee.role === 'admin' ? 'مدير' :\n employee.role === 'supervisor' ? 'مشرف' : 'موظف'\n )\n )\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}\n , _optionalChain([locations, 'access', _29 => _29.find, 'call', _30 => _30(l => l.id === employee.locationId), 'optionalAccess', _31 => _31.name]) || '-'\n )\n , React.createElement('td', { className: \"px-6 py-4 text-gray-600\" ,}, employee.phone || '-')\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('span', { className: `px-2 py-1 text-xs font-medium rounded-full ${\n employee.active !== false ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'\n }`,}\n , employee.active !== false ? 'نشط' : 'غير نشط'\n )\n )\n , React.createElement('td', { className: \"px-6 py-4\" ,}\n , React.createElement('div', { className: \"flex items-center gap-2\" ,}\n , React.createElement('button', {\n onClick: () => handleEditEmployee(employee),\n className: \"p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z\" ,} )\n )\n )\n , React.createElement('button', {\n onClick: () => handleDeleteEmployee(employee.id),\n className: \"p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors\" ,}\n\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16\" ,} )\n )\n )\n )\n )\n )\n ))\n )\n )\n )\n )\n )\n\n /* Modal */\n , showModal && (\n React.createElement(EmployeeModal, {\n employee: editingEmployee,\n locations: locations,\n onClose: () => setShowModal(false),\n onSave: loadData,}\n )\n )\n )\n )\n}\n\nfunction EmployeeModal({ employee, locations, onClose, onSave }) {\n const [formData, setFormData] = useState({\n name: '',\n email: '',\n phone: '',\n role: 'employee',\n locationId: '',\n active: true\n })\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState('')\n\n useEffect(() => {\n if (employee) {\n setFormData({\n name: employee.name || '',\n email: employee.email || '',\n phone: employee.phone || '',\n role: employee.role || 'employee',\n locationId: employee.locationId || '',\n active: employee.active !== false\n })\n }\n }, [employee])\n\n const handleSubmit = async (e) => {\n e.preventDefault()\n setLoading(true)\n setError('')\n\n try {\n if (employee) {\n await window.db.update('employees', employee.id, formData)\n } else {\n await window.db.create('employees', {\n ...formData,\n createdAt: new Date().toISOString()\n })\n }\n onSave()\n onClose()\n } catch (err) {\n setError('فشل حفظ البيانات')\n } finally {\n setLoading(false)\n }\n }\n\n return (\n React.createElement('div', { className: \"fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50\" ,}\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto\" ,}\n , React.createElement('div', { className: \"p-6 border-b border-gray-200\" ,}\n , React.createElement('h3', { className: \"text-xl font-bold text-gray-900\" ,}\n , employee ? 'تعديل الموظف' : 'إضافة موظف جديد'\n )\n )\n\n , React.createElement('form', { onSubmit: handleSubmit, className: \"p-6 space-y-4\" ,}\n , error && (\n React.createElement('div', { className: \"p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm\" ,}\n , error\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الاسم الكامل\" )\n , React.createElement('input', {\n type: \"text\",\n required: true,\n value: formData.name,\n onChange: (e) => setFormData({...formData, name: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"البريد الإلكتروني\" )\n , React.createElement('input', {\n type: \"email\",\n required: true,\n value: formData.email,\n onChange: (e) => setFormData({...formData, email: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"رقم الهاتف\" )\n , React.createElement('input', {\n type: \"tel\",\n value: formData.phone,\n onChange: (e) => setFormData({...formData, phone: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الدور\")\n , React.createElement('select', {\n value: formData.role,\n onChange: (e) => setFormData({...formData, role: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n\n , React.createElement('option', { value: \"employee\",}, \"موظف\")\n , React.createElement('option', { value: \"supervisor\",}, \"مشرف\")\n , React.createElement('option', { value: \"admin\",}, \"مدير\")\n )\n )\n\n , React.createElement('div', null\n , React.createElement('label', { className: \"block text-sm font-medium text-gray-700 mb-2\" ,}, \"الموقع\")\n , React.createElement('select', {\n value: formData.locationId,\n onChange: (e) => setFormData({...formData, locationId: e.target.value}),\n className: \"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent\" ,}\n\n , React.createElement('option', { value: \"\",}, \"اختر الموقع\" )\n , locations.map(loc => (\n React.createElement('option', { key: loc.id, value: loc.id,}, loc.name)\n ))\n )\n )\n\n , React.createElement('div', { className: \"flex items-center gap-2\" ,}\n , React.createElement('input', {\n type: \"checkbox\",\n id: \"active\",\n checked: formData.active,\n onChange: (e) => setFormData({...formData, active: e.target.checked}),\n className: \"w-4 h-4 text-blue-600 rounded focus:ring-blue-500\" ,}\n )\n , React.createElement('label', { htmlFor: \"active\", className: \"text-sm text-gray-700\" ,}, \"نشط\")\n )\n\n , React.createElement('div', { className: \"flex gap-3 pt-4\" ,}\n , React.createElement('button', {\n type: \"button\",\n onClick: onClose,\n className: \"flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors\" ,}\n, \"إلغاء\"\n\n )\n , React.createElement('button', {\n type: \"submit\",\n disabled: loading,\n className: \"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50\" ,}\n\n , loading ? 'جاري الحفظ...' : 'حفظ'\n )\n )\n )\n )\n )\n )\n}\n\n// Component: AttendanceView\nfunction AttendanceView({ user }) {\n const [location, setLocation] = useState(null)\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState('')\n const [todayRecord, setTodayRecord] = useState(null)\n const [checkingIn, setCheckingIn] = useState(false)\n const [checkingOut, setCheckingOut] = useState(false)\n\n useEffect(() => {\n loadTodayRecord()\n }, [])\n\n const loadTodayRecord = async () => {\n try {\n const today = new Date().toISOString().split('T')[0]\n const result = await window.db.query('attendance', {\n filter: { employeeId: user.id, date: today }\n })\n \n if (result.items && result.items.length > 0) {\n setTodayRecord(result.items[0])\n }\n } catch (err) {\n console.error('Error loading today record:', err)\n } finally {\n setLoading(false)\n }\n }\n\n const getCurrentLocation = () => {\n return new Promise((resolve, reject) => {\n if (!navigator.geolocation) {\n reject(new Error('المتصفح لا يدعم تحديد الموقع'))\n return\n }\n\n navigator.geolocation.getCurrentPosition(\n (position) => {\n resolve({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracy: position.coords.accuracy\n })\n },\n (error) => {\n let message = 'فشل تحديد الموقع'\n switch (error.code) {\n case error.PERMISSION_DENIED:\n message = 'تم رفض إذن تحديد الموقع'\n break\n case error.POSITION_UNAVAILABLE:\n message = 'معلومات الموقع غير متاحة'\n break\n case error.TIMEOUT:\n message = 'انتهت مهلة تحديد الموقع'\n break\n }\n reject(new Error(message))\n },\n {\n enableHighAccuracy: true,\n timeout: 10000,\n maximumAge: 0\n }\n )\n })\n }\n\n const validateLocation = async (currentLocation) => {\n try {\n const result = await window.db.query('locations')\n const locations = result.items || []\n\n for (const loc of locations) {\n const distance = calculateDistance(\n currentLocation.latitude,\n currentLocation.longitude,\n loc.latitude,\n loc.longitude\n )\n\n if (distance <= loc.radius) {\n return { valid: true, location: loc, distance }\n }\n }\n\n return { valid: false, distance: null }\n } catch (err) {\n console.error('Error validating location:', err)\n return { valid: false, error: err.message }\n }\n }\n\n const calculateDistance = (lat1, lon1, lat2, lon2) => {\n const R = 6371 // Earth's radius in km\n const dLat = (lat2 - lat1) * Math.PI / 180\n const dLon = (lon2 - lon1) * Math.PI / 180\n const a = \n Math.sin(dLat/2) * Math.sin(dLat/2) +\n Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * \n Math.sin(dLon/2) * Math.sin(dLon/2)\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))\n return R * c * 1000 // Return in meters\n }\n\n const handleCheckIn = async () => {\n setError('')\n setCheckingIn(true)\n\n try {\n const currentLocation = await getCurrentLocation()\n setLocation(currentLocation)\n\n const validation = await validateLocation(currentLocation)\n\n if (!validation.valid) {\n setError('أنت خارج نطاق الموقع المصرح به')\n return\n }\n\n const now = new Date()\n const workStartTime = new Date()\n workStartTime.setHours(9, 0, 0, 0) // 9:00 AM\n\n const isLate = now > workStartTime\n\n const record = {\n employeeId: user.id,\n employeeName: user.name,\n locationId: validation.location.id,\n locationName: validation.location.name,\n date: now.toISOString().split('T')[0],\n checkInTime: now.toISOString(),\n checkOutTime: null,\n type: 'check-in',\n status: 'present',\n isLate,\n latitude: currentLocation.latitude,\n longitude: currentLocation.longitude,\n accuracy: currentLocation.accuracy,\n distanceFromLocation: validation.distance\n }\n\n const result = await window.db.create('attendance', record)\n \n if (result.success) {\n setTodayRecord({ ...record, id: result.id })\n \n // Send WhatsApp notification\n await sendWhatsAppNotification(user, 'check-in', validation.location.name)\n }\n } catch (err) {\n setError(err.message || 'فشل تسجيل الحضور')\n } finally {\n setCheckingIn(false)\n }\n }\n\n const handleCheckOut = async () => {\n setError('')\n setCheckingOut(true)\n\n try {\n const currentLocation = await getCurrentLocation()\n setLocation(currentLocation)\n\n const validation = await validateLocation(currentLocation)\n\n if (!validation.valid) {\n setError('أنت خارج نطاق الموقع المصرح به')\n return\n }\n\n const now = new Date()\n const checkInTime = new Date(todayRecord.checkInTime)\n const workHours = (now - checkInTime) / (1000 * 60 * 60) // Hours\n\n await window.db.update('attendance', todayRecord.id, {\n checkOutTime: now.toISOString(),\n type: 'check-out',\n workHours,\n latitude: currentLocation.latitude,\n longitude: currentLocation.longitude\n })\n\n setTodayRecord({\n ...todayRecord,\n checkOutTime: now.toISOString(),\n workHours\n })\n\n // Send WhatsApp notification\n await sendWhatsAppNotification(user, 'check-out', todayRecord.locationName)\n } catch (err) {\n setError(err.message || 'فشل تسجيل الانصراف')\n } finally {\n setCheckingOut(false)\n }\n }\n\n const sendWhatsAppNotification = async (user, type, locationName) => {\n // This would integrate with WhatsApp API\n console.log(`WhatsApp notification: ${type} for ${user.name} at ${locationName}`)\n }\n\n if (loading) {\n return (\n React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-8\" ,}\n , React.createElement('div', { className: \"text-center py-12\" ,}\n , React.createElement('div', { className: \"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4\" ,})\n , React.createElement('p', { className: \"text-gray-600\",}, \"جاري التحميل...\" )\n )\n )\n )\n }\n\n return (\n React.createElement('div', { className: \"space-y-6\",}\n , React.createElement('div', { className: \"flex items-center justify-between\" ,}\n , React.createElement('h2', { className: \"text-2xl font-bold text-gray-900\" ,}, \"تسجيل الحضور والانصراف\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}\n , new Date().toLocaleDateString('ar-SA', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })\n )\n )\n\n /* Location Status */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('div', { className: \"flex items-center gap-4 mb-4\" ,}\n , React.createElement('div', { className: \"w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-6 h-6 text-blue-600\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n )\n )\n , React.createElement('div', null\n , React.createElement('h3', { className: \"font-semibold text-gray-900\" ,}, \"حالة الموقع\" )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, \"يجب أن تكون داخل نطاق العمل المصرح به\" )\n )\n )\n\n , location && (\n React.createElement('div', { className: \"bg-gray-50 rounded-lg p-4 text-sm\" ,}\n , React.createElement('p', { className: \"text-gray-600 mb-1\" ,}\n , React.createElement('span', { className: \"font-medium\",}, \"خط العرض:\" ), \" \" , location.latitude.toFixed(6)\n )\n , React.createElement('p', { className: \"text-gray-600 mb-1\" ,}\n , React.createElement('span', { className: \"font-medium\",}, \"خط الطول:\" ), \" \" , location.longitude.toFixed(6)\n )\n , React.createElement('p', { className: \"text-gray-600\",}\n , React.createElement('span', { className: \"font-medium\",}, \"الدقة:\"), \" ±\" , location.accuracy.toFixed(0), \" متر\"\n )\n )\n )\n )\n\n /* Check-in/Check-out Buttons */\n , React.createElement('div', { className: \"grid grid-cols-1 md:grid-cols-2 gap-4\" ,}\n , !todayRecord || !todayRecord.checkInTime ? (\n React.createElement('button', {\n onClick: handleCheckIn,\n disabled: checkingIn,\n className: \"flex items-center justify-center gap-3 p-6 bg-green-500 hover:bg-green-600 disabled:bg-green-300 text-white rounded-xl transition-all transform hover:scale-[1.02]\" ,}\n\n , React.createElement('div', { className: \"w-12 h-12 bg-white/20 rounded-full flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-6 h-6\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1\" ,} )\n )\n )\n , React.createElement('div', { className: \"text-right\",}\n , React.createElement('p', { className: \"text-xl font-bold\" ,}, \"تسجيل الدخول\" )\n , React.createElement('p', { className: \"text-sm opacity-90\" ,}, checkingIn ? 'جاري التسجيل...' : 'اضغط لتسجيل الحضور')\n )\n )\n ) : (\n React.createElement('button', {\n onClick: handleCheckOut,\n disabled: checkingOut || !!todayRecord.checkOutTime,\n className: \"flex items-center justify-center gap-3 p-6 bg-red-500 hover:bg-red-600 disabled:bg-red-300 text-white rounded-xl transition-all transform hover:scale-[1.02]\" ,}\n\n , React.createElement('div', { className: \"w-12 h-12 bg-white/20 rounded-full flex items-center justify-center\" ,}\n , React.createElement('svg', { className: \"w-6 h-6\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1\" ,} )\n )\n )\n , React.createElement('div', { className: \"text-right\",}\n , React.createElement('p', { className: \"text-xl font-bold\" ,}, \"تسجيل الخروج\" )\n , React.createElement('p', { className: \"text-sm opacity-90\" ,}, checkingOut ? 'جاري التسجيل...' : 'اضغط لتسجيل الانصراف')\n )\n )\n )\n\n /* Status Display */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('h3', { className: \"font-semibold text-gray-900 mb-4\" ,}, \"حالة اليوم\" )\n , todayRecord ? (\n React.createElement('div', { className: \"space-y-3\",}\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"وقت الدخول:\" )\n , React.createElement('span', { className: \"font-medium\",}\n , todayRecord.checkInTime ? new Date(todayRecord.checkInTime).toLocaleTimeString('ar-SA') : '-'\n )\n )\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"وقت الخروج:\" )\n , React.createElement('span', { className: \"font-medium\",}\n , todayRecord.checkOutTime ? new Date(todayRecord.checkOutTime).toLocaleTimeString('ar-SA') : '-'\n )\n )\n , React.createElement('div', { className: \"flex justify-between\" ,}\n , React.createElement('span', { className: \"text-gray-500\",}, \"ساعات العمل:\" )\n , React.createElement('span', { className: \"font-medium\",}\n , todayRecord.workHours ? todayRecord.workHours.toFixed(2) + ' ساعة' : '-'\n )\n )\n , todayRecord.isLate && (\n React.createElement('div', { className: \"flex items-center gap-2 text-yellow-600\" ,}\n , React.createElement('svg', { className: \"w-4 h-4\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" ,} )\n )\n , React.createElement('span', { className: \"text-sm font-medium\" ,}, \"تأخرت اليوم\" )\n )\n )\n )\n ) : (\n React.createElement('p', { className: \"text-gray-500 text-center py-4\" ,}, \"لم تسجل الدخول اليوم\" )\n )\n )\n )\n\n , error && (\n React.createElement('div', { className: \"bg-red-50 border border-red-200 rounded-lg p-4 text-red-600\" ,}\n , React.createElement('div', { className: \"flex items-center gap-2\" ,}\n , React.createElement('svg', { className: \"w-5 h-5\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" ,} )\n )\n , React.createElement('span', null, error)\n )\n )\n )\n\n /* Recent Attendance */\n , React.createElement('div', { className: \"bg-white rounded-xl shadow-sm p-6\" ,}\n , React.createElement('h3', { className: \"font-semibold text-gray-900 mb-4\" ,}, \"سجل الحضور الأخير\" )\n , React.createElement(AttendanceHistory, { userId: user.id,} )\n )\n )\n )\n}\n\nfunction AttendanceHistory({ userId }) {\n const [records, setRecords] = useState([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n loadRecords()\n }, [userId])\n\n const loadRecords = async () => {\n try {\n const result = await window.db.query('attendance', {\n filter: { employeeId: userId },\n sort: 'createdAt',\n order: 'desc',\n limit: 10\n })\n setRecords(result.items || [])\n } catch (err) {\n console.error('Error loading records:', err)\n } finally {\n setLoading(false)\n }\n }\n\n if (loading) {\n return React.createElement('div', { className: \"text-center py-4 text-gray-500\" ,}, \"جاري التحميل...\" )\n }\n\n if (records.length === 0) {\n return React.createElement('div', { className: \"text-center py-4 text-gray-500\" ,}, \"لا يوجد سجلات\" )\n }\n\n return (\n React.createElement('div', { className: \"space-y-2\",}\n , records.map((record) => (\n React.createElement('div', { key: record.id, className: \"flex items-center justify-between p-3 bg-gray-50 rounded-lg\" ,}\n , React.createElement('div', null\n , React.createElement('p', { className: \"font-medium text-gray-900\" ,}\n , new Date(record.date).toLocaleDateString('ar-SA', { weekday: 'long', day: 'numeric', month: 'long' })\n )\n , React.createElement('p', { className: \"text-sm text-gray-500\" ,}, record.locationName)\n )\n , React.createElement('div', { className: \"text-left\",}\n , React.createElement('p', { className: \"text-sm\",}\n , React.createElement('span', { className: \"text-green-600\",}, \"دخول:\"), \" \" , new Date(record.checkInTime).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' })\n )\n , record.checkOutTime && (\n React.createElement('p', { className: \"text-sm\",}\n , React.createElement('span', { className: \"text-red-600\",}, \"خروج:\"), \" \" , new Date(record.checkOutTime).toLocaleTimeString('ar-SA', { hour: '2-digit', minute: '2-digit' })\n )\n )\n )\n )\n ))\n )\n )\n}\n\n\n// Main Page\n function LandingPage() {\n return (\n React.createElement('div', { className: \"min-h-screen bg-gradient-to-br from-blue-900 via-blue-800 to-indigo-900 flex items-center justify-center p-4\" ,}\n , React.createElement('div', { className: \"max-w-4xl w-full text-center text-white\" ,}\n , React.createElement('div', { className: \"mb-8\",}\n , React.createElement('div', { className: \"inline-flex items-center justify-center w-20 h-20 bg-white/10 rounded-full mb-6\" ,}\n , React.createElement('svg', { className: \"w-10 h-10\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M15 11a3 3 0 11-6 0 3 3 0 016 0z\" ,} )\n )\n )\n , React.createElement('h1', { className: \"text-4xl md:text-5xl font-bold mb-4\" ,}, \"نظام الحضور بالمواقع الجغرافية\" )\n , React.createElement('p', { className: \"text-xl text-blue-200 mb-2\" ,}, \"Geolocation Attendance System\" )\n )\n\n , React.createElement('div', { className: \"bg-white/10 backdrop-blur-lg rounded-2xl p-8 mb-8\" ,}\n , React.createElement('h2', { className: \"text-2xl font-semibold mb-6\" ,}, \"المميزات الرئيسية\" )\n , React.createElement('div', { className: \"grid md:grid-cols-3 gap-6 text-right\" ,}\n , React.createElement('div', { className: \"bg-white/5 rounded-xl p-6\" ,}\n , React.createElement('div', { className: \"w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center mb-4 mx-auto\" ,}\n , React.createElement('svg', { className: \"w-6 h-6 text-green-400\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\" ,} )\n )\n )\n , React.createElement('h3', { className: \"font-semibold mb-2\" ,}, \"تتبع GPS دقيق\" )\n , React.createElement('p', { className: \"text-sm text-blue-200\" ,}, \"تحديد الموقع بدقة عالية باستخدام GPS\" )\n )\n\n , React.createElement('div', { className: \"bg-white/5 rounded-xl p-6\" ,}\n , React.createElement('div', { className: \"w-12 h-12 bg-yellow-500/20 rounded-lg flex items-center justify-center mb-4 mx-auto\" ,}\n , React.createElement('svg', { className: \"w-6 h-6 text-yellow-400\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\" ,} )\n )\n )\n , React.createElement('h3', { className: \"font-semibold mb-2\" ,}, \"التحقق الجغرافي\" )\n , React.createElement('p', { className: \"text-sm text-blue-200\" ,}, \"تسجيل الحضور فقط داخل المناطق المصرح بها\" )\n )\n\n , React.createElement('div', { className: \"bg-white/5 rounded-xl p-6\" ,}\n , React.createElement('div', { className: \"w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center mb-4 mx-auto\" ,}\n , React.createElement('svg', { className: \"w-6 h-6 text-purple-400\" , fill: \"none\", stroke: \"currentColor\", viewBox: \"0 0 24 24\" ,}\n , React.createElement('path', { strokeLinecap: \"round\", strokeLinejoin: \"round\", strokeWidth: 2, d: \"M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" ,} )\n )\n )\n , React.createElement('h3', { className: \"font-semibold mb-2\" ,}, \"تقارير شاملة\" )\n , React.createElement('p', { className: \"text-sm text-blue-200\" ,}, \"تقارير يومية وأسبوعية وشهرية مفصلة\" )\n )\n )\n )\n\n , React.createElement('div', { className: \"flex flex-col sm:flex-row gap-4 justify-center\" ,}\n , React.createElement('button', {\n onClick: () => window.location.href = '/auth',\n className: \"px-8 py-4 bg-white text-blue-900 font-semibold rounded-xl hover:bg-blue-50 transition-all transform hover:scale-105 shadow-lg\" ,}\n, \"تسجيل الدخول\"\n\n )\n , React.createElement('button', {\n onClick: () => window.location.href = '/auth?mode=signup',\n className: \"px-8 py-4 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-500 transition-all transform hover:scale-105 border-2 border-white/30\" ,}\n, \"إنشاء حساب جديد\"\n\n )\n )\n\n , React.createElement('p', { className: \"mt-8 text-blue-300 text-sm\" ,}, \"نظام آمن ومحمي للتتبع الجغرافي للحضور والانصراف\"\n\n )\n )\n )\n )\n} exports.default = LandingPage;"; // 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 '32b4be3b-4bec-4a9a-83cd-629b689362cd'; }; // 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': '32b4be3b-4bec-4a9a-83cd-629b689362cd' }, 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', '32b4be3b-4bec-4a9a-83cd-629b689362cd'); 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/32b4be3b-4bec-4a9a-83cd-629b689362cd'; } 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: '32b4be3b-4bec-4a9a-83cd-629b689362cd' }); 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 }, '*'); }); } } } })();