diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..14e510f 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,247 @@ +:root { + --color-bg: #f7f7f5; + --color-surface: #ffffff; + --color-surface-2: #efefec; + --color-text: #171717; + --color-muted: #686868; + --color-border: #deded9; + --color-accent: #111111; + --color-accent-2: #525252; + --radius-xs: 4px; + --radius-sm: 8px; + --radius-md: 12px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 14px 40px rgba(0, 0, 0, 0.08); +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + margin: 0; + background: var(--color-bg); + color: var(--color-text); + font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 15px; + line-height: 1.55; + text-rendering: optimizeLegibility; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.skip-link { + left: 1rem; + position: absolute; + top: -4rem; + z-index: 2000; + background: var(--color-text); + color: #fff; + padding: .55rem .8rem; + border-radius: var(--radius-xs); } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.skip-link:focus { top: 1rem; } + +a { color: var(--color-text); text-underline-offset: 3px; } + +.navbar { min-height: 66px; } +.navbar-brand { font-weight: 800; letter-spacing: -0.03em; } +.nav-link { color: #353535; font-weight: 600; font-size: .92rem; } +.nav-link:hover, .nav-link:focus { color: #000; } +.brand-mark { + align-items: center; + background: #111; + border-radius: var(--radius-xs); + color: #fff; + display: inline-flex; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .78rem; + height: 28px; + justify-content: center; + letter-spacing: -.08em; + width: 28px; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.hero-section { background: var(--color-surface); } +.py-lg-6 { padding-bottom: 5.5rem; padding-top: 5.5rem; } +.text-balance { text-wrap: balance; } +.lead { font-size: 1.08rem; max-width: 720px; } +.eyebrow { + color: #4d4d4d; + font-size: .75rem; + font-weight: 800; + letter-spacing: .14em; + text-transform: uppercase; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.btn { border-radius: var(--radius-sm); font-weight: 700; } +.btn-lg { --bs-btn-padding-y: .78rem; --bs-btn-padding-x: 1rem; --bs-btn-font-size: .98rem; } +.btn-dark { background: var(--color-accent); border-color: var(--color-accent); } +.btn-dark:hover { background: #2b2b2b; border-color: #2b2b2b; } +.btn-outline-secondary { border-color: #c9c9c3; color: #252525; } + +.hero-panel, .app-shell, .provider-card { + background: var(--color-surface); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.hero-panel { border: 1px solid var(--color-border); padding: 1.15rem; } +.panel-topline { color: var(--color-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .82rem; } +.status-dot { color: #1f6b3a; font-weight: 800; } +.code-window { + background: #151515; + border: 1px solid #2b2b2b; + border-radius: var(--radius-sm); + color: #f6f6f3; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .9rem; + line-height: 1.9; + overflow-x: auto; + padding: 1rem; +} +.code-window .muted { color: #9ca3af; } +.code-window .string { color: #d8d8d2; } +.runtime-grid { display: grid; gap: .75rem; grid-template-columns: repeat(2, minmax(0, 1fr)); } +.runtime-grid div { background: var(--color-surface-2); border-radius: var(--radius-sm); padding: .75rem; } +.runtime-grid dt { color: var(--color-muted); font-size: .72rem; font-weight: 800; text-transform: uppercase; } +.runtime-grid dd { font-size: .88rem; font-weight: 700; margin: .1rem 0 0; } + +.section-pad { padding: 4.5rem 0; } +.bg-subtle { background: var(--color-surface-2); } +.narrow { max-width: 560px; } +.section-heading { max-width: 760px; } + +.app-shell { overflow: hidden; } +.search-card { padding: 1.15rem; } +.form-label { color: #303030; font-size: .82rem; font-weight: 800; } +.form-control, .form-select { + border-color: #cfcfca; + border-radius: var(--radius-sm); + color: var(--color-text); +} +.form-control:focus, .form-select:focus, .btn:focus-visible, .chip:focus-visible, .result-item:focus-visible, .language-card:focus-visible { + border-color: #111; + box-shadow: 0 0 0 .2rem rgba(17, 17, 17, .14); + outline: 0; +} +.quick-searches { display: flex; flex-wrap: wrap; gap: .5rem; } +.chip { + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: 999px; + color: #252525; + cursor: pointer; + font-size: .82rem; + font-weight: 700; + padding: .4rem .7rem; +} +.chip:hover { background: #e7e7e1; } + +.results-column { background: #fbfbfa; min-height: 540px; } +.detail-column { background: var(--color-surface); min-height: 540px; } +.results-toolbar { + align-items: center; + border-bottom: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 1rem; +} +.results-list { display: grid; gap: .65rem; padding: 1rem; } +.result-item { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + cursor: pointer; + padding: .95rem; + text-align: left; + transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease; + width: 100%; +} +.result-item:hover { border-color: #b9b9b3; box-shadow: var(--shadow-sm); transform: translateY(-1px); } +.result-item.active { border-color: #111; box-shadow: 0 0 0 1px #111 inset; } +.result-title { font-weight: 800; letter-spacing: -.02em; margin-bottom: .25rem; } +.result-excerpt { color: var(--color-muted); font-size: .88rem; margin-bottom: .75rem; } +.badge-soft { + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: 999px; + color: #333; + display: inline-block; + font-size: .72rem; + font-weight: 800; + margin: 0 .3rem .3rem 0; + padding: .25rem .5rem; +} +.score { color: var(--color-muted); font-size: .78rem; font-weight: 700; } +.detail-panel { min-height: 100%; padding: 1.25rem; } +.empty-state { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 420px; + text-align: center; +} +.empty-icon { + align-items: center; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + display: inline-flex; + font-size: 1.5rem; + height: 54px; + justify-content: center; + margin-bottom: 1rem; + width: 54px; +} +.detail-meta { align-items: center; display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: 1rem; } +.detail-summary { color: #333; font-size: 1rem; } +.code-block { + background: #151515; + border-radius: var(--radius-sm); + color: #f7f7f5; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .86rem; + overflow-x: auto; + padding: 1rem; + white-space: pre; +} +.detail-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; } + +.language-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + color: var(--color-text); + cursor: pointer; + display: flex; + flex-direction: column; + min-height: 112px; + padding: 1rem; + text-align: left; + transition: border-color .15s ease, transform .15s ease; + width: 100%; +} +.language-card:hover { border-color: #111; transform: translateY(-1px); } +.language-card span { font-size: 1rem; font-weight: 800; } +.language-card small { color: var(--color-muted); margin-top: .35rem; } +.provider-card { padding: 1.25rem; } +.checklist { color: #333; padding-left: 1.2rem; } +.checklist li + li { margin-top: .55rem; } +.site-footer { background: var(--color-surface); color: #333; font-size: .9rem; } + +@media (max-width: 991.98px) { + .py-lg-6 { padding-bottom: 3.5rem; padding-top: 3.5rem; } + .section-pad { padding: 3.25rem 0; } + .results-column { border-right: 0 !important; min-height: auto; } + .detail-column { border-top: 1px solid var(--color-border); min-height: auto; } } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +@media (max-width: 575.98px) { + body { font-size: 14px; } + .display-5 { font-size: 2.1rem; } + .runtime-grid { grid-template-columns: 1fr; } + .results-toolbar { align-items: flex-start; flex-direction: column; } } - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..b2a578b 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,262 @@ +const lessons = [ + { + id: 'go-json-parse', + title: 'Parse JSON into a Go struct', + language: 'Go', + tags: ['json', 'struct', 'api'], + excerpt: 'Use encoding/json with exported struct fields and explicit error handling.', + summary: 'Go parses JSON by unmarshalling bytes into a struct. Field names must be exported, and struct tags let you map JSON keys to Go names.', + code: `type Payload struct {\n Name string \`json:"name"\`\n}\n\nvar p Payload\nif err := json.Unmarshal(body, &p); err != nil {\n return err\n}` + }, + { + id: 'js-async-await', + title: 'Use async/await for readable JavaScript APIs', + language: 'JavaScript', + tags: ['async', 'fetch', 'promises'], + excerpt: 'Wrap fetch calls in async functions, await the response, then handle errors explicitly.', + summary: 'async/await keeps asynchronous JavaScript code close to synchronous control flow. Always check response.ok and catch failures near the calling boundary.', + code: `async function loadUser(id) {\n const res = await fetch(\`/api/users/\${id}\`);\n if (!res.ok) throw new Error('Request failed');\n return await res.json();\n}` + }, + { + id: 'python-list-comprehension', + title: 'Filter and transform Python lists', + language: 'Python', + tags: ['lists', 'comprehension', 'clean code'], + excerpt: 'List comprehensions are concise when the expression and predicate stay simple.', + summary: 'Use a list comprehension when you can express mapping and filtering in one readable line. Prefer a normal loop when you need multiple steps or side effects.', + code: `names = ['Ada', 'Linus', 'Grace']\nshort_names = [name.lower() for name in names if len(name) <= 5]` + }, + { + id: 'sql-joins', + title: 'Choose the right SQL join', + language: 'SQL', + tags: ['joins', 'select', 'relational'], + excerpt: 'INNER JOIN returns matching rows; LEFT JOIN keeps all rows from the left table.', + summary: 'Start with INNER JOIN when both records must exist. Use LEFT JOIN for optional related records, such as users without orders yet.', + code: `SELECT users.name, orders.total\nFROM users\nLEFT JOIN orders ON orders.user_id = users.id\nWHERE users.active = 1;` + }, + { + id: 'php-pdo-prepared', + title: 'Run safe PHP PDO queries', + language: 'PHP', + tags: ['pdo', 'security', 'mysql'], + excerpt: 'Prepared statements prevent SQL injection and keep user input out of raw SQL strings.', + summary: 'Create a PDO statement, bind values, execute it, and fetch typed results. Never concatenate untrusted request input into SQL.', + code: `$stmt = $pdo->prepare('SELECT * FROM posts WHERE slug = :slug');\n$stmt->execute(['slug' => $slug]);\n$post = $stmt->fetch(PDO::FETCH_ASSOC);` + }, + { + id: 'ts-generics', + title: 'Model reusable TypeScript functions with generics', + language: 'TypeScript', + tags: ['types', 'generics', 'safety'], + excerpt: 'Generics preserve specific types while keeping helpers reusable.', + summary: 'Use a generic type parameter when the function should work with many input types but return or store the same specific type information.', + code: `function first(items: T[]): T | undefined {\n return items[0];\n}\n\nconst value = first(['docs', 'code']);` + } +]; + +const els = {}; +let activeResults = []; +let selectedId = null; +let toastInstance = null; + +function $(selector) { return document.querySelector(selector); } + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (char) => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[char])); +} + +function scoreLesson(lesson, query, language) { + const q = query.trim().toLowerCase(); + const haystack = `${lesson.title} ${lesson.language} ${lesson.tags.join(' ')} ${lesson.excerpt} ${lesson.summary}`.toLowerCase(); + let score = 0; + if (language === 'all' || lesson.language === language) score += 25; + if (language !== 'all' && lesson.language !== language) return 0; + if (!q) return score; + const terms = q.split(/\s+/).filter(Boolean); + terms.forEach((term) => { + if (lesson.title.toLowerCase().includes(term)) score += 18; + if (lesson.tags.join(' ').toLowerCase().includes(term)) score += 12; + if (haystack.includes(term)) score += 6; + }); + return score; +} + +function searchLessons(query, language) { + return lessons + .map((lesson) => ({ ...lesson, score: scoreLesson(lesson, query, language) })) + .filter((lesson) => lesson.score > 0) + .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title)); +} + +function renderResults(results) { + activeResults = results; + els.resultCount.textContent = results.length ? `${results.length} result${results.length === 1 ? '' : 's'} ranked by relevance.` : 'No matches yet. Try another language or broader topic.'; + if (!results.length) { + const hasQuery = els.query && els.query.value.trim().length >= 2; + els.resultsList.innerHTML = hasQuery + ? `

No results found

Try “JSON”, “async”, “join”, or choose All languages.

` + : `

Start with a query

Search a topic or choose a suggested chip to preview the knowledge workflow.

`; + renderEmptyDetail(); + return; + } + els.resultsList.innerHTML = results.map((lesson, index) => ` + + `).join(''); +} + +function renderEmptyDetail() { + selectedId = null; + els.detail.innerHTML = `

Choose a result

Open a result to see a concise explanation, tags, and copy-ready code examples.

`; +} + +function renderDetail(id) { + const lesson = lessons.find(item => item.id === id); + if (!lesson) return renderEmptyDetail(); + selectedId = id; + els.detail.innerHTML = ` +
+ ${escapeHtml(lesson.language)} + ${lesson.tags.map(tag => `${escapeHtml(tag)}`).join('')} +
+

${escapeHtml(lesson.title)}

+

${escapeHtml(lesson.summary)}

+

Code example

+
${escapeHtml(lesson.code)}
+
+ + +
+ `; + renderResults(activeResults); + updateUrl({ result: id }); +} + +function updateUrl(extra = {}) { + const params = new URLSearchParams(window.location.search); + const query = els.query.value.trim(); + const language = els.language.value; + if (query) params.set('q', query); else params.delete('q'); + if (language && language !== 'all') params.set('language', language); else params.delete('language'); + if (extra.result) params.set('result', extra.result); + const next = `${window.location.pathname}${params.toString() ? `?${params}` : ''}#search`; + history.replaceState({}, '', next); +} + +function showToast(message) { + els.toastMessage.textContent = message; + if (window.bootstrap && toastInstance) toastInstance.show(); +} + +function submitSearch(showNotification = true) { + const query = els.query.value.trim(); + if (query.length < 2) { + els.form.classList.add('was-validated'); + els.query.focus(); + return; + } + els.form.classList.remove('was-validated'); + selectedId = null; + const results = searchLessons(query, els.language.value); + renderResults(results); + if (results[0]) renderDetail(results[0].id); + updateUrl(); + if (showNotification) showToast('Search complete. Results ranked locally for the MVP demo.'); +} + +async function copyText(text, message) { + try { + await navigator.clipboard.writeText(text); + showToast(message); + } catch (error) { + showToast('Copy failed. You can manually copy from the page.'); + } +} + +function initFromUrl() { + const params = new URLSearchParams(window.location.search); + const q = params.get('q'); + const language = params.get('language'); + const result = params.get('result'); + if (q) els.query.value = q; + if (language && [...els.language.options].some(option => option.value === language || option.textContent === language)) els.language.value = language; + if (q && q.length >= 2) { + const results = searchLessons(q, els.language.value); + renderResults(results); + renderDetail(result && lessons.some(item => item.id === result) ? result : (results[0]?.id || null)); + } +} + +function bindEvents() { + els.form.addEventListener('submit', (event) => { + event.preventDefault(); + submitSearch(); + }); + + document.addEventListener('click', (event) => { + const resultButton = event.target.closest('[data-id]'); + if (resultButton) { + renderDetail(resultButton.dataset.id); + document.getElementById('detail').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + return; + } + + const chip = event.target.closest('.chip'); + if (chip) { + els.query.value = chip.dataset.query || ''; + els.language.value = chip.dataset.language || 'all'; + submitSearch(); + return; + } + + const languageCard = event.target.closest('.language-card'); + if (languageCard) { + els.language.value = languageCard.dataset.language; + els.query.value = languageCard.dataset.language; + submitSearch(); + document.getElementById('search').scrollIntoView({ behavior: 'smooth' }); + return; + } + + const copyCode = event.target.closest('[data-copy-code]'); + if (copyCode) { + const lesson = lessons.find(item => item.id === copyCode.dataset.copyCode); + if (lesson) copyText(lesson.code, 'Code example copied.'); + return; + } + + const shareResult = event.target.closest('[data-share-result]'); + if (shareResult) { + updateUrl({ result: shareResult.dataset.shareResult }); + copyText(window.location.href, 'Shareable result link copied.'); + } + }); + + els.copySearchLink.addEventListener('click', () => { + updateUrl(selectedId ? { result: selectedId } : {}); + copyText(window.location.href, 'Shareable search link copied.'); + }); +} + document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } - }); + els.form = $('#searchForm'); + els.query = $('#queryInput'); + els.language = $('#languageFilter'); + els.resultsList = $('#resultsList'); + els.resultCount = $('#resultCount'); + els.detail = $('#detail'); + els.copySearchLink = $('#copySearchLink'); + els.toastMessage = $('#toastMessage'); + const toastEl = $('#appToast'); + if (window.bootstrap && toastEl) toastInstance = new bootstrap.Toast(toastEl, { delay: 2800 }); + renderResults([]); + bindEvents(); + initFromUrl(); }); diff --git a/index.php b/index.php index 7205f3d..207ec8f 100644 --- a/index.php +++ b/index.php @@ -1,9 +1,10 @@ @@ -12,139 +13,210 @@ $now = date('Y-m-d H:i:s'); - New Style - + <?= htmlspecialchars($projectName) ?> — Programming Search Platform - - - + - - - + + + - - + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… + + +
+ + + +
+
+
+
+
+
Programming language knowledge search
+

Search code answers across languages from one precise workspace.

+

A first MVP slice for an AI-style developer search platform: enter a programming question, filter by language, inspect ranked results, open a structured explanation, and share the search URL.

+ +
+
+ +
+
+
+
+ + + +
+
+
+
+

Browse

+

Language categories

+
+

These cards drive quick filters now and can become React routes or Algolia facets later.

+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+

External search provider

+

Ready for Google CSE or Algolia

+

This first slice intentionally avoids a backend/database. The UI is provider-ready: swap the local demo index in assets/js/main.js with Google Custom Search JSON API or Algolia InstantSearch when keys are available.

+
+
+
+

Configuration checklist

+
    +
  1. Create a Google Programmable Search Engine or Algolia app.
  2. +
  3. Restrict API keys by domain before production use.
  4. +
  5. Replace the demo search adapter with the provider client.
  6. +
  7. Keep query, language, and result id in the URL for sharing.
  8. +
+
+
+
+
+
-