Feil
Syntax error in template "string:

{ "explanation": "I built a complete ..." on line 7 "

\n\n" }, { "filename": "css/style.css", "language": "css", "content": ":root {\n --bg: #0b0e14;\n --surface: #141a24;\n --surface-2: #1c2430;\n --border: #2a3442;\n --text: #e6edf3;\n --text-muted: #8b98a9;\n --accent: #7c5cff;\n --accent-hover: #9b7fff;\n --success: #2dd4bf;\n --danger: #f87171;\n --warning: #fbbf24;\n --radius: 12px;\n --shadow: 0 4px 20px rgba(0,0,0,0.3);\n --font-display: 'Space Grotesk', sans-serif;\n --font-body: 'Inter', sans-serif;\n}\n\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n font-family: var(--font-body);\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n}\n\na {\n color: var(--accent);\n text-decoration: none;\n}\n\na:hover {\n text-decoration: underline;\n}\n\nbutton {\n font-family: var(--font-body);\n cursor: pointer;\n border: none;\n background: none;\n color: inherit;\n}\n\ninput, textarea, select {\n font-family: var(--font-body);\n background: var(--surface-2);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 10px 14px;\n border-radius: 8px;\n font-size: 14px;\n width: 100%;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\ninput:focus, textarea:focus, select:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.2);\n}\n\nlabel {\n font-size: 13px;\n font-weight: 500;\n color: var(--text-muted);\n display: block;\n margin-bottom: 6px;\n}\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 10px 20px;\n border-radius: 8px;\n font-weight: 600;\n font-size: 14px;\n transition: background 0.2s, transform 0.1s, box-shadow 0.2s;\n border: 1px solid transparent;\n}\n\n.btn-primary {\n background: var(--accent);\n color: #fff;\n}\n\n.btn-primary:hover {\n background: var(--accent-hover);\n box-shadow: 0 4px 15px rgba(124, 92, 255, 0.4);\n}\n\n.btn-secondary {\n background: var(--surface-2);\n border-color: var(--border);\n color: var(--text);\n}\n\n.btn-secondary:hover {\n background: var(--border);\n}\n\n.btn-danger {\n background: transparent;\n border-color: var(--danger);\n color: var(--danger);\n}\n\n.btn-danger:hover {\n background: rgba(248, 113, 113, 0.1);\n}\n\n.btn-sm {\n padding: 6px 12px;\n font-size: 13px;\n}\n\n.card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 24px;\n box-shadow: var(--shadow);\n}\n\n.form-group {\n margin-bottom: 18px;\n}\n\n.form-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 16px;\n}\n\n@media (max-width: 600px) {\n .form-row {\n grid-template-columns: 1fr;\n }\n}\n\n/* Layout */\n.app-shell {\n display: flex;\n min-height: 100vh;\n}\n\n.sidebar {\n width: 240px;\n background: var(--surface);\n border-right: 1px solid var(--border);\n padding: 24px 16px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sidebar .logo {\n font-family: var(--font-display);\n font-size: 22px;\n font-weight: 700;\n color: var(--accent);\n margin-bottom: 24px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sidebar .logo span {\n font-size: 24px;\n}\n\n.nav-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-radius: 8px;\n color: var(--text-muted);\n font-weight: 500;\n transition: background 0.2s, color 0.2s;\n cursor: pointer;\n border: none;\n background: none;\n width: 100%;\n text-align: left;\n font-size: 14px;\n}\n\n.nav-item:hover {\n background: var(--surface-2);\n color: var(--text);\n}\n\n.nav-item.active {\n background: rgba(124, 92, 255, 0.15);\n color: var(--accent);\n}\n\n.nav-item .icon {\n font-size: 18px;\n}\n\n.main {\n flex: 1;\n padding: 32px;\n overflow-y: auto;\n}\n\n.page-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.page-header h1 {\n font-family: var(--font-display);\n font-size: 28px;\n font-weight: 600;\n}\n\n.page-header p {\n color: var(--text-muted);\n font-size: 14px;\n}\n\n/* Dashboard */\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n margin-bottom: 24px;\n}\n\n.stat-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 20px;\n}\n\n.stat-card .label {\n font-size: 13px;\n color: var(--text-muted);\n margin-bottom: 8px;\n}\n\n.stat-card .value {\n font-family: var(--font-display);\n font-size: 32px;\n font-weight: 700;\n}\n\n.stat-card .trend {\n font-size: 13px;\n margin-top: 8px;\n color: var(--success);\n}\n\n/* Tables */\n.table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.table th, .table td {\n text-align: left;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 14px;\n}\n\n.table th {\n color: var(--text-muted);\n font-weight: 500;\n font-size: 13px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.table tr:hover td {\n background: rgba(255,255,255,0.02);\n}\n\n.status-badge {\n display: inline-block;\n padding: 4px 10px;\n border-radius: 20px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.status-active {\n background: rgba(45, 212, 191, 0.15);\n color: var(--success);\n}\n\n.status-paused {\n background: rgba(251, 191, 36, 0.15);\n color: var(--warning);\n}\n\n.status-completed {\n background: rgba(124, 92, 255, 0.15);\n color: var(--accent);\n}\n\n/* Activity log */\n.activity-list {\n list-style: none;\n}\n\n.activity-item {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 12px 0;\n border-bottom: 1px solid var(--border);\n}\n\n.activity-item:last-child {\n border-bottom: none;\n}\n\n.activity-icon {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--surface-2);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.activity-content {\n flex: 1;\n}\n\n.activity-content .text {\n font-size: 14px;\n}\n\n.activity-content .time {\n font-size: 12px;\n color: var(--text-muted);\n}\n\n/* Modal */\n.modal-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0,0,0,0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n padding: 20px;\n}\n\n.modal {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n max-width: 600px;\n width: 100%;\n padding: 24px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.5);\n}\n\n.modal h2 {\n font-family: var(--font-display);\n font-size: 22px;\n margin-bottom: 16px;\n}\n\n.modal-actions {\n display: flex;\n justify-content: flex-end;\n gap: 12px;\n margin-top: 20px;\n}\n\n/* Login */\n.login-container {\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--bg);\n padding: 20px;\n}\n\n.login-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 16px;\n padding: 40px;\n max-width: 400px;\n width: 100%;\n box-shadow: 0 20px 60px rgba(0,0,0,0.4);\n}\n\n.login-card h1 {\n font-family: var(--font-display);\n font-size: 28px;\n margin-bottom: 8px;\n color: var(--accent);\n}\n\n.login-card p {\n color: var(--text-muted);\n margin-bottom: 24px;\n}\n\n.login-card .btn {\n width: 100%;\n margin-top: 8px;\n}\n\n.error-text {\n color: var(--danger);\n font-size: 13px;\n margin-top: 8px;\n}\n\n.empty-state {\n text-align: center;\n padding: 40px 20px;\n color: var(--text-muted);\n}\n\n.empty-state .icon {\n font-size: 48px;\n margin-bottom: 12px;\n}\n\n.empty-state h3 {\n font-family: var(--font-display);\n color: var(--text);\n margin-bottom: 8px;\n}\n\n/* Toast */\n.toast-container {\n position: fixed;\n bottom: 20px;\n right: 20px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n z-index: 2000;\n}\n\n.toast {\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 12px 20px;\n box-shadow: var(--shadow);\n font-size: 14px;\n animation: slideIn 0.3s ease;\n}\n\n.toast.success {\n border-left: 4px solid var(--success);\n}\n\n.toast.error {\n border-left: 4px solid var(--danger);\n}\n\n@keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .app-shell {\n flex-direction: column;\n }\n .sidebar {\n width: 100%;\n flex-direction: row;\n align-items: center;\n overflow-x: auto;\n padding: 12px;\n border-right: none;\n border-bottom: 1px solid var(--border);\n }\n .sidebar .logo {\n margin-bottom: 0;\n margin-right: 16px;\n }\n .nav-item {\n width: auto;\n white-space: nowrap;\n }\n .main {\n padding: 16px;\n }\n .page-header {\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n }\n}\n" }, { "filename": "js/app.js", "language": "javascript", "content": "// CommentBot - Frontend application\n// Uses localStorage as a simulated backend for demo purposes.\n\nconst STORAGE_KEYS = {\n SESSION: 'commentbot_session',\n CAMPAIGNS: 'commentbot_campaigns',\n ACTIVITY: 'commentbot_activity',\n TEMPLATES: 'commentbot_templates',\n TARGETS: 'commentbot_targets'\n};\n\nconst DEFAULT_TEMPLATES = [\n 'Great post! Thanks for sharing.',\n 'This is really insightful. I learned a lot.',\n 'Totally agree with your point. Well said!',\n 'Thanks for the valuable content. Keep it up!'\n];\n\nconst DEFAULT_TARGETS = [\n 'https://www.reddit.com/r/technology/comments/example1',\n 'https://news.ycombinator.com/item?id=12345',\n 'https://www.linkedin.com/posts/example-company/example-post'\n];\n\n// ---------- Storage helpers ----------\nfunction getData(key, fallback) {\n try {\n const raw = localStorage.getItem(key);\n return raw ? JSON.parse(raw) : fallback;\n } catch (e) {\n console.error('Failed to parse storage key', key, e);\n return fallback;\n }\n}\n\nfunction setData(key, value) {\n try {\n localStorage.setItem(key, JSON.stringify(value));\n } catch (e) {\n console.error('Failed to save storage key', key, e);\n showToast('Failed to save data. Storage might be full.', 'error');\n }\n}\n\n// ---------- Auth ----------\nfunction getSession() {\n return getData(STORAGE_KEYS.SESSION, null);\n}\n\nfunction setSession(user) {\n setData(STORAGE_KEYS.SESSION, user);\n}\n\nfunction clearSession() {\n localStorage.removeItem(STORAGE_KEYS.SESSION);\n}\n\n// ---------- Campaigns ----------\nfunction getCampaigns() {\n return getData(STORAGE_KEYS.CAMPAIGNS, []);\n}\n\nfunction saveCampaigns(campaigns) {\n setData(STORAGE_KEYS.CAMPAIGNS, campaigns);\n}\n\nfunction getCampaignById(id) {\n return getCampaigns().find(c => c.id === id);\n}\n\nfunction createCampaign(data) {\n const campaigns = getCampaigns();\n const campaign = {\n id: 'camp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),\n name: data.name,\n platform: data.platform,\n status: 'active',\n targets: data.targets || [],\n templateIds: data.templateIds || [],\n interval: parseInt(data.interval, 10) || 60,\n createdAt: new Date().toISOString(),\n commentsPosted: 0\n };\n campaigns.push(campaign);\n saveCampaigns(campaigns);\n logActivity(`Campaign \"${campaign.name}\" created`, 'create');\n return campaign;\n}\n\nfunction updateCampaign(id, updates) {\n const campaigns = getCampaigns();\n const idx = campaigns.findIndex(c => c.id === id);\n if (idx === -1) return null;\n campaigns[idx] = { ...campaigns[idx], ...updates };\n saveCampaigns(campaigns);\n return campaigns[idx];\n}\n\nfunction deleteCampaign(id) {\n const campaigns = getCampaigns();\n const campaign = campaigns.find(c => c.id === id);\n const filtered = campaigns.filter(c => c.id !== id);\n saveCampaigns(filtered);\n if (campaign) logActivity(`Campaign \"${campaign.name}\" deleted`, 'delete');\n}\n\nfunction toggleCampaignStatus(id) {\n const campaign = getCampaignById(id);\n if (!campaign) return;\n const newStatus = campaign.status === 'active' ? 'paused' : 'active';\n updateCampaign(id, { status: newStatus });\n logActivity(`Campaign \"${campaign.name}\" ${newStatus === 'active' ? 'activated' : 'paused'}`, 'status');\n}\n\n// ---------- Templates ----------\nfunction getTemplates() {\n return getData(STORAGE_KEYS.TEMPLATES, DEFAULT_TEMPLATES);\n}\n\nfunction saveTemplates(templates) {\n setData(STORAGE_KEYS.TEMPLATES, templates);\n}\n\nfunction addTemplate(text) {\n const templates = getTemplates();\n templates.push(text);\n saveTemplates(templates);\n logActivity('New comment template added', 'template');\n}\n\nfunction deleteTemplate(index) {\n const templates = getTemplates();\n if (index < 0 || index >= templates.length) return;\n templates.splice(index, 1);\n saveTemplates(templates);\n logActivity('Comment template removed', 'template');\n}\n\n// ---------- Targets ----------\nfunction getTargets() {\n return getData(STORAGE_KEYS.TARGETS, DEFAULT_TARGETS);\n}\n\nfunction saveTargets(targets) {\n setData(STORAGE_KEYS.TARGETS, targets);\n}\n\nfunction addTarget(url) {\n const targets = getTargets();\n targets.push(url);\n saveTargets(targets);\n logActivity('New target post added', 'target');\n}\n\nfunction deleteTarget(index) {\n const targets = getTargets();\n if (index < 0 || index >= targets.length) return;\n targets.splice(index, 1);\n saveTargets(targets);\n logActivity('Target post removed', 'target');\n}\n\n// ---------- Activity ----------\nfunction getActivity() {\n return getData(STORAGE_KEYS.ACTIVITY, []);\n}\n\nfunction logActivity(text, type = 'info') {\n const activity = getActivity();\n activity.unshift({\n id: 'act_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),\n text,\n type,\n timestamp: new Date().toISOString()\n });\n // Keep only last 50 entries\n const trimmed = activity.slice(0, 50);\n setData(STORAGE_KEYS.ACTIVITY, trimmed);\n}\n\n// ---------- Toast ----------\nfunction showToast(message, type = 'success') {\n const container = document.querySelector('.toast-container');\n if (!container) return;\n const toast = document.createElement('div');\n toast.className = `toast ${type}`;\n toast.textContent = message;\n container.appendChild(toast);\n setTimeout(() => {\n toast.style.opacity = '0';\n toast.style.transition = 'opacity 0.3s';\n setTimeout(() => toast.remove(), 300);\n }, 3000);\n}\n\n// ---------- Router ----------\nconst routes = {\n login: renderLogin,\n dashboard: renderDashboard,\n campaigns: renderCampaigns,\n templates: renderTemplates,\n targets: renderTargets,\n activity: renderActivity\n};\n\nlet currentRoute = 'login';\n\nfunction navigate(route) {\n if (!routes[route]) {\n console.error('Unknown route:', route);\n return;\n }\n currentRoute = route;\n const app = document.getElementById('app');\n app.innerHTML = '';\n routes[route]();\n // Update active nav\n document.querySelectorAll('.nav-item').forEach(item => {\n item.classList.toggle('active', item.dataset.route === route);\n });\n}\n\n// ---------- Renderers ----------\nfunction renderLogin() {\n const app = document.getElementById('app');\n app.innerHTML = `\n

" - Unexpected ": ", expected one of: "}"