From c0d8a58377a0887f55b9c00690fee9530bb9917f Mon Sep 17 00:00:00 2001 From: Logan Lindquist Land Date: Sun, 13 Sep 2026 10:28:48 -0500 Subject: [PATCH 1/3] Add frontend quality checks --- .github/workflows/ci.yml | 25 + bench/lib/auth.js | 22 +- bench/post-messages.js | 20 +- bench/read-messages.js | 6 +- bench/websocket-fanout.js | 14 +- biome.json | 36 ++ docs/contributing.md | 18 +- e2e/global-setup.ts | 6 +- e2e/support/chat-scenarios.ts | 16 +- package.json | 12 +- playwright.config.ts | 2 +- pnpm-lock.yaml | 247 +++++++++- src/js/chat-html.js | 90 ++-- src/js/chat-json.js | 543 +++++++++++----------- src/js/chat.js | 459 +++++++++--------- src/svelte/App.svelte | 93 ++-- src/svelte/main.js | 38 +- src/vue/App.vue | 844 +++++++++++++++++----------------- src/vue/main.js | 4 +- svelte.config.js | 5 + tsconfig.vue.json | 13 + vite.config.js | 34 +- 22 files changed, 1419 insertions(+), 1128 deletions(-) create mode 100644 biome.json create mode 100644 svelte.config.js create mode 100644 tsconfig.vue.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae3b374..5dbfbd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,31 @@ on: branches: [master] jobs: + frontend: + name: Frontend checks + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 11 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19' + cache: pnpm + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Run frontend checks + run: pnpm check + test: name: PHP ${{ matrix.php }} runs-on: ubuntu-latest diff --git a/bench/lib/auth.js b/bench/lib/auth.js index 8d0d12f..e0c02e5 100644 --- a/bench/lib/auth.js +++ b/bench/lib/auth.js @@ -1,6 +1,6 @@ -import http from 'k6/http'; import { check, fail } from 'k6'; import exec from 'k6/execution'; +import http from 'k6/http'; export const baseUrl = (__ENV.BASE_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''); export const websocketUrl = (__ENV.WS_URL || 'ws://127.0.0.1:8081').replace(/\/$/, ''); @@ -20,14 +20,18 @@ export function loginForUser(sequence) { fail(`Could not read the CSRF token from ${baseUrl}/auth/login`); } - const response = http.post(`${baseUrl}/auth/processLogin`, { - username, - password, - csrf_test_name: loginCsrf, - }, { - redirects: 5, - tags: { name: 'POST /auth/processLogin' }, - }); + const response = http.post( + `${baseUrl}/auth/processLogin`, + { + username, + password, + csrf_test_name: loginCsrf, + }, + { + redirects: 5, + tags: { name: 'POST /auth/processLogin' }, + }, + ); const authenticated = check(response, { 'login succeeds': (result) => result.status === 200 && result.url.includes('/chat'), }); diff --git a/bench/post-messages.js b/bench/post-messages.js index 3a0d267..e7228cd 100644 --- a/bench/post-messages.js +++ b/bench/post-messages.js @@ -1,5 +1,5 @@ -import http from 'k6/http'; import { check, sleep } from 'k6'; +import http from 'k6/http'; import { Rate, Trend } from 'k6/metrics'; import { baseUrl, csrfToken, login } from './lib/auth.js'; @@ -25,13 +25,17 @@ export const options = { export default function () { session ||= login(); - const response = http.post(`${baseUrl}/api/v1/messages`, { - message: `k6 post ${session.username} ${Date.now()}`, - csrf_test_name: csrfToken(), - }, { - headers: { Accept: 'application/json' }, - tags: { name: 'POST /api/v1/messages' }, - }); + const response = http.post( + `${baseUrl}/api/v1/messages`, + { + message: `k6 post ${session.username} ${Date.now()}`, + csrf_test_name: csrfToken(), + }, + { + headers: { Accept: 'application/json' }, + tags: { name: 'POST /api/v1/messages' }, + }, + ); const succeeded = check(response, { 'message post succeeds': (result) => result.status === 200 && result.json('success') === true, }); diff --git a/bench/read-messages.js b/bench/read-messages.js index 524431e..654ea1e 100644 --- a/bench/read-messages.js +++ b/bench/read-messages.js @@ -1,11 +1,11 @@ -import http from 'k6/http'; import { check, sleep } from 'k6'; +import http from 'k6/http'; import { Rate, Trend } from 'k6/metrics'; import { baseUrl, login } from './lib/auth.js'; const failures = new Rate('read_message_failures'); const latency = new Trend('read_message_duration', true); -let session; +let _session; export const options = { noCookiesReset: true, @@ -24,7 +24,7 @@ export const options = { }; export default function () { - session ||= login(); + _session ||= login(); const response = http.get(`${baseUrl}/api/v1/messages?page=1&per_page=50`, { headers: { Accept: 'application/json' }, tags: { name: 'GET /api/v1/messages' }, diff --git a/bench/websocket-fanout.js b/bench/websocket-fanout.js index bbd063a..3f83d48 100644 --- a/bench/websocket-fanout.js +++ b/bench/websocket-fanout.js @@ -1,6 +1,6 @@ import { check } from 'k6'; -import { Rate, Trend } from 'k6/metrics'; import exec from 'k6/execution'; +import { Rate, Trend } from 'k6/metrics'; import { WebSocket } from 'k6/websockets'; import { loginForUser, websocketUrl } from './lib/auth.js'; @@ -48,11 +48,13 @@ export default function (sessions) { socket.addEventListener('open', () => { setTimeout(() => { sentAt = Date.now(); - socket.send(JSON.stringify({ - action: 'sendMessage', - username: session.username, - message: marker, - })); + socket.send( + JSON.stringify({ + action: 'sendMessage', + username: session.username, + message: marker, + }), + ); }, 2_000); }); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c43ed36 --- /dev/null +++ b/biome.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["*.js", "*.ts", "src/**/*.js", "src/**/*.vue", "src/**/*.svelte", "e2e/**/*.ts", "bench/**/*.js"], + "ignoreUnknown": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + }, + "html": { + "experimentalFullSupportEnabled": true, + "formatter": { + "enabled": true, + "indentScriptAndStyle": true + } + } +} diff --git a/docs/contributing.md b/docs/contributing.md index 7d5d409..8e66564 100755 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -18,7 +18,7 @@ Thank you for your interest in contributing to this project! This is a learning php spark serve # In a separate terminal, start Vite for frontend assets -npm run dev +pnpm dev # Optional: Start the WebSocket server for real-time features php spark websocket:start @@ -43,7 +43,7 @@ composer test:coverage ### Code Quality -This project uses PHPStan for static analysis and PHP-CS-Fixer for code style: +This project uses PHPStan and PHP-CS-Fixer for PHP, plus Biome, `svelte-check`, and `vue-tsc` for frontend code: ```bash # Run static analysis @@ -54,13 +54,20 @@ composer cs-check # Automatically fix code style issues composer cs-fix + +# Lint and format-check JavaScript, then check Svelte and Vue components +pnpm check + +# Apply safe Biome formatting and lint fixes +pnpm format ``` -Please ensure your code passes both static analysis and code style checks before submitting a pull request. +Please ensure the PHP and frontend checks pass before submitting a pull request. Biome covers repository JavaScript and TypeScript; framework-aware diagnostics are handled separately by `svelte-check` and `vue-tsc`. ## Code Style Guidelines - Follow PSR-12 coding standards (enforced by PHP-CS-Fixer) +- Keep JavaScript and TypeScript formatted and lint-clean with Biome - Use meaningful variable and function names - Add comments for complex logic - Keep functions focused and small @@ -70,8 +77,9 @@ Please ensure your code passes both static analysis and code style checks before 1. Ensure all tests pass (`composer test`) 2. Run static analysis (`composer analyse`) 3. Fix any code style issues (`composer cs-fix`) -4. Update documentation if needed -5. Write a clear description of your changes +4. Run frontend checks (`pnpm check`) +5. Update documentation if needed +6. Write a clear description of your changes ## Project Structure diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index d37abc6..671a5f4 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -1,4 +1,4 @@ -import { execFileSync, spawn, type ChildProcess } from 'node:child_process'; +import { type ChildProcess, execFileSync, spawn } from 'node:child_process'; import fs from 'node:fs'; import net from 'node:net'; import path from 'node:path'; @@ -57,7 +57,7 @@ function assertPortFree(port: number): Promise { reject(new Error(`Port ${port} is already in use; refusing to run E2E against an unknown server`)); }); server.listen(port, '127.0.0.1', () => { - server.close(error => { + server.close((error) => { if (error) { reject(error); return; @@ -117,7 +117,7 @@ function stopProcess(child: ChildProcess): Promise { return Promise.resolve(); } - return new Promise(resolve => { + return new Promise((resolve) => { const timeout = setTimeout(() => { child.kill('SIGKILL'); resolve(); diff --git a/e2e/support/chat-scenarios.ts b/e2e/support/chat-scenarios.ts index 89b2450..75e332b 100644 --- a/e2e/support/chat-scenarios.ts +++ b/e2e/support/chat-scenarios.ts @@ -1,5 +1,5 @@ import AxeBuilder from '@axe-core/playwright'; -import { expect, test, type Page, type Response } from '@playwright/test'; +import { expect, type Page, type Response, test } from '@playwright/test'; type NetworkFormat = 'json' | 'xml'; @@ -20,16 +20,14 @@ export function chatScenario(scenario: ChatScenario): void { await login(page); const browserErrors: string[] = []; - page.on('pageerror', error => browserErrors.push(error.message)); - page.on('console', message => { + page.on('pageerror', (error) => browserErrors.push(error.message)); + page.on('console', (message) => { if (message.type() === 'error') { browserErrors.push(message.text()); } }); - const payloadPromise = scenario.network - ? captureNetworkPayload(page, scenario.network) - : null; + const payloadPromise = scenario.network ? captureNetworkPayload(page, scenario.network) : null; await page.goto(scenario.path); @@ -95,9 +93,9 @@ function captureNetworkPayload(page: Page, network: NonNullable { const results = await new AxeBuilder({ page }).analyze(); - const blocking = results.violations.filter(violation => - violation.impact === 'critical' || violation.impact === 'serious', + const blocking = results.violations.filter( + (violation) => violation.impact === 'critical' || violation.impact === 'serious', ); - expect(blocking, blocking.map(violation => `${violation.id}: ${violation.help}`).join('\n')).toEqual([]); + expect(blocking, blocking.map((violation) => `${violation.id}: ${violation.help}`).join('\n')).toEqual([]); } diff --git a/package.json b/package.json index f523f90..31a94e6 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "CodeIgniter Chat Application", "private": true, "type": "module", + "packageManager": "pnpm@11.9.0", "engines": { "node": ">=20.19" }, @@ -14,18 +15,27 @@ "bench:post": "k6 run bench/post-messages.js", "bench:read": "k6 run bench/read-messages.js", "bench:ws": "k6 run bench/websocket-fanout.js", + "check": "pnpm check:biome && pnpm check:svelte && pnpm check:vue", + "check:biome": "biome check --error-on-warnings .", + "check:svelte": "svelte-check --config ./svelte.config.js --fail-on-warnings", + "check:vue": "vue-tsc --noEmit -p tsconfig.vue.json", + "format": "biome check --write .", "e2e": "playwright test", "e2e:ui": "playwright test --ui", "e2e:codegen": "playwright codegen http://127.0.0.1:8085" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", + "@biomejs/biome": "^2.5.13", "@playwright/test": "^1.63.0", "@sveltejs/vite-plugin-svelte": "^7.3.0", "@vitejs/plugin-vue": "^6.0.8", "sass": "^1.104.0", "svelte": "^5.57.0", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", "vite": "^8.2.2", - "vue": "^3.5.42" + "vue": "^3.5.42", + "vue-tsc": "^3.3.11" } } diff --git a/playwright.config.ts b/playwright.config.ts index 6171629..ac4cc51 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,5 @@ -import { defineConfig, devices } from '@playwright/test'; import fs from 'node:fs'; +import { defineConfig, devices } from '@playwright/test'; import { applicationEnvironment, baseURL, cachePath, rootDir } from './e2e/support/environment'; fs.mkdirSync(cachePath, { recursive: true }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5214835..8ce88ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@axe-core/playwright': specifier: ^4.13.0 version: 4.13.0(playwright-core@1.63.0) + '@biomejs/biome': + specifier: ^2.5.13 + version: 2.5.13 '@playwright/test': specifier: ^1.63.0 version: 1.63.0 @@ -22,19 +25,28 @@ importers: version: 7.3.0(svelte@5.57.0)(vite@8.2.2(sass@1.104.0)) '@vitejs/plugin-vue': specifier: ^6.0.8 - version: 6.0.8(vite@8.2.2(sass@1.104.0))(vue@3.5.42) + version: 6.0.8(vite@8.2.2(sass@1.104.0))(vue@3.5.42(typescript@6.0.3)) sass: specifier: ^1.104.0 version: 1.104.0 svelte: specifier: ^5.57.0 version: 5.57.0 + svelte-check: + specifier: ^4.7.6 + version: 4.7.6(picomatch@4.0.5)(svelte@5.57.0)(typescript@6.0.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 vite: specifier: ^8.2.2 version: 8.2.2(sass@1.104.0) vue: specifier: ^3.5.42 - version: 3.5.42 + version: 3.5.42(typescript@6.0.3) + vue-tsc: + specifier: ^3.3.11 + version: 3.3.11(typescript@6.0.3) packages: @@ -60,6 +72,63 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} + '@biomejs/biome@2.5.13': + resolution: {integrity: sha512-+SEC/mFk1a+5mvUANZgbZTaiZXs1nj4iMhL/PHiqDT5TPUEPFIlliEtkKKZB4N862yylHC3UI+/Sj2I0HJEqhA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.13': + resolution: {integrity: sha512-nYSuDJ6zgVqZUkAkJkvhXxsF2PYIrUk/g638K4voCXz7foI9f7b6C2/7+oAihsHQlivZNMUrm/y8ywlcHQtZOw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.13': + resolution: {integrity: sha512-KVy1ceEDuJ3AzFxjT9kkxbVy+UANw1pjEMUS6lvKfxjJ+fmkRvc7sQn1Xo6ETgseEI7wUQoV03KSga3XfE8YRQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.13': + resolution: {integrity: sha512-CH32xpep3dNS5EVJpAHlYchkBYznxyVZQrx0b6YYYlPL9u9ZeD2NtqiK/6s64+HFS2a7hGnryLlqqZAOh8ax5g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.13': + resolution: {integrity: sha512-VlNMtoxOqs0dUR6drxxHr18SNUvI7xxAuHZlH4s/dstYSf3g6RaqVgo8JRnhcOhKz9afWrvtJTboNG1JuYlIDQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.13': + resolution: {integrity: sha512-F3pmwl+VHoUuVJN/tbNLKeLt0SVK3EIyN1jXlMcNyQGYRQQTVqXF+GgkP0/CjGXFN87e/mv0sBfUnWqWza5PKQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.13': + resolution: {integrity: sha512-Fi6gIxbUaJ3ZCIXeG3ggIBPF76O2DjDN67WrPX/ODRv54GeXPm2gbEPC96Yb0yrJoiH0Eax1JLx1Pi0XbsNFZQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.13': + resolution: {integrity: sha512-+WD13qshXrr0Icv4BfsAdzm8Fs3TL+nZ59zEQxsYNd8lcgZJ0A5+OrlwsL1PipVCmWeRpxgIPD6DAcEd7smkCQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.13': + resolution: {integrity: sha512-VOofU/nW761XWzUeUNE8zzYNyPrxuMuNFMizL0qz7J75yeV8N7WFheUlk1/K6dOkaFpH9wJ+lDKlwjAbw0346w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -273,6 +342,10 @@ packages: peerDependencies: acorn: ^8.9.0 + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + '@sveltejs/vite-plugin-svelte@7.3.0': resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -290,6 +363,15 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue/compiler-core@3.5.42': resolution: {integrity: sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==} @@ -302,6 +384,9 @@ packages: '@vue/compiler-ssr@3.5.42': resolution: {integrity: sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==} + '@vue/language-core@3.3.11': + resolution: {integrity: sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==} + '@vue/reactivity@3.5.42': resolution: {integrity: sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==} @@ -322,6 +407,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -334,6 +422,10 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -485,6 +577,13 @@ packages: magic-string@1.2.0: resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -497,6 +596,9 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -522,6 +624,10 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + readdirp@5.1.1: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} @@ -531,6 +637,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + sass@1.104.0: resolution: {integrity: sha512-btHMApW2bgolvClhRW8AlQJzgI9lUB3pPSofBQQT+E46GWvf9o0TVQ13SYv5riWZVFyPN+JNz3TKW9XhBlc10w==} engines: {node: '>=20.19.0'} @@ -540,6 +650,14 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + svelte@5.57.0: resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} engines: {node: '>=18'} @@ -548,6 +666,11 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -599,6 +722,15 @@ packages: vite: optional: true + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + vue-tsc@3.3.11: + resolution: {integrity: sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + vue@3.5.42: resolution: {integrity: sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==} peerDependencies: @@ -630,6 +762,41 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@biomejs/biome@2.5.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.13 + '@biomejs/cli-darwin-x64': 2.5.13 + '@biomejs/cli-linux-arm64': 2.5.13 + '@biomejs/cli-linux-arm64-musl': 2.5.13 + '@biomejs/cli-linux-x64': 2.5.13 + '@biomejs/cli-linux-x64-musl': 2.5.13 + '@biomejs/cli-win32-arm64': 2.5.13 + '@biomejs/cli-win32-x64': 2.5.13 + + '@biomejs/cli-darwin-arm64@2.5.13': + optional: true + + '@biomejs/cli-darwin-x64@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64@2.5.13': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-x64@2.5.13': + optional: true + + '@biomejs/cli-win32-arm64@2.5.13': + optional: true + + '@biomejs/cli-win32-x64@2.5.13': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -765,6 +932,8 @@ snapshots: dependencies: acorn: 8.18.0 + '@sveltejs/load-config@0.2.3': {} + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(sass@1.104.0))': dependencies: deepmerge: 4.3.1 @@ -776,11 +945,23 @@ snapshots: '@types/estree@1.0.9': {} - '@vitejs/plugin-vue@6.0.8(vite@8.2.2(sass@1.104.0))(vue@3.5.42)': + '@vitejs/plugin-vue@6.0.8(vite@8.2.2(sass@1.104.0))(vue@3.5.42(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: 8.2.2(sass@1.104.0) - vue: 3.5.42 + vue: 3.5.42(typescript@6.0.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 '@vue/compiler-core@3.5.42': dependencies: @@ -812,6 +993,16 @@ snapshots: '@vue/compiler-dom': 3.5.42 '@vue/shared': 3.5.42 + '@vue/language-core@3.3.11': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.42 + '@vue/shared': 3.5.42 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.7 + '@vue/reactivity@3.5.42': dependencies: '@vue/shared': 3.5.42 @@ -838,12 +1029,18 @@ snapshots: acorn@8.18.0: {} + alien-signals@3.2.1: {} + aria-query@5.3.1: {} axe-core@4.13.0: {} axobject-query@4.1.0: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -948,6 +1145,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mri@1.2.0: {} + + muggle-string@0.4.1: {} + nanoid@3.3.18: {} node-addon-api@7.1.1: @@ -955,12 +1156,13 @@ snapshots: obug@2.1.4: {} + path-browserify@1.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.5: {} - picomatch@4.0.7: - optional: true + picomatch@4.0.7: {} playwright-core@1.63.0: {} @@ -974,6 +1176,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + readdirp@4.1.2: {} + readdirp@5.1.1: {} rolldown@1.2.5: @@ -997,6 +1201,10 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.5 '@rolldown/binding-win32-x64-msvc': 1.2.5 + sade@1.8.1: + dependencies: + mri: 1.2.0 + sass@1.104.0: dependencies: chokidar: 5.0.0 @@ -1007,6 +1215,19 @@ snapshots: source-map-js@1.2.1: {} + svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.57.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + svelte@5.57.0: dependencies: '@jridgewell/remapping': 2.3.5 @@ -1032,6 +1253,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + typescript@6.0.3: {} + vite@8.2.2(sass@1.104.0): dependencies: lightningcss: 1.33.0 @@ -1047,12 +1270,22 @@ snapshots: optionalDependencies: vite: 8.2.2(sass@1.104.0) - vue@3.5.42: + vscode-uri@3.2.0: {} + + vue-tsc@3.3.11(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.11 + typescript: 6.0.3 + + vue@3.5.42(typescript@6.0.3): dependencies: '@vue/compiler-dom': 3.5.42 '@vue/compiler-sfc': 3.5.42 '@vue/runtime-dom': 3.5.42 '@vue/server-renderer': 3.5.42 '@vue/shared': 3.5.42 + optionalDependencies: + typescript: 6.0.3 zimmerframe@1.1.4: {} diff --git a/src/js/chat-html.js b/src/js/chat-html.js index 0704165..cb67ff3 100644 --- a/src/js/chat-html.js +++ b/src/js/chat-html.js @@ -5,54 +5,54 @@ // Import CSS import '../css/chat.css'; -document.addEventListener('DOMContentLoaded', function() { - const form = document.getElementById('chatform'); - const nameInput = document.getElementById('name'); - const contentInput = document.getElementById('content'); - const nameError = document.getElementById('name-error'); - const contentError = document.getElementById('content-error'); +document.addEventListener('DOMContentLoaded', () => { + const form = document.getElementById('chatform'); + const nameInput = document.getElementById('name'); + const contentInput = document.getElementById('content'); + const nameError = document.getElementById('name-error'); + const contentError = document.getElementById('content-error'); - form.addEventListener('submit', function(e) { - let isValid = true; + form.addEventListener('submit', (e) => { + let isValid = true; - // Reset errors - nameError.textContent = ''; - contentError.textContent = ''; - nameInput.classList.remove('error-field'); - contentInput.classList.remove('error-field'); + // Reset errors + nameError.textContent = ''; + contentError.textContent = ''; + nameInput.classList.remove('error-field'); + contentInput.classList.remove('error-field'); - // Validate name - if (!nameInput.value.trim()) { - nameError.textContent = 'Name is required'; - nameInput.classList.add('error-field'); - isValid = false; - } else if (nameInput.value.trim().length < 2) { - nameError.textContent = 'Name must be at least 2 characters long'; - nameInput.classList.add('error-field'); - isValid = false; - } else if (nameInput.value.trim().length > 50) { - nameError.textContent = 'Name cannot exceed 50 characters'; - nameInput.classList.add('error-field'); - isValid = false; - } else if (!/^[a-zA-Z0-9 ]+$/.test(nameInput.value.trim())) { - nameError.textContent = 'Name can only contain alphanumeric characters and spaces'; - nameInput.classList.add('error-field'); - isValid = false; - } + // Validate name + if (!nameInput.value.trim()) { + nameError.textContent = 'Name is required'; + nameInput.classList.add('error-field'); + isValid = false; + } else if (nameInput.value.trim().length < 2) { + nameError.textContent = 'Name must be at least 2 characters long'; + nameInput.classList.add('error-field'); + isValid = false; + } else if (nameInput.value.trim().length > 50) { + nameError.textContent = 'Name cannot exceed 50 characters'; + nameInput.classList.add('error-field'); + isValid = false; + } else if (!/^[a-zA-Z0-9 ]+$/.test(nameInput.value.trim())) { + nameError.textContent = 'Name can only contain alphanumeric characters and spaces'; + nameInput.classList.add('error-field'); + isValid = false; + } - // Validate message - if (!contentInput.value.trim()) { - contentError.textContent = 'Message is required'; - contentInput.classList.add('error-field'); - isValid = false; - } else if (contentInput.value.trim().length > 500) { - contentError.textContent = 'Message cannot exceed 500 characters'; - contentInput.classList.add('error-field'); - isValid = false; - } + // Validate message + if (!contentInput.value.trim()) { + contentError.textContent = 'Message is required'; + contentInput.classList.add('error-field'); + isValid = false; + } else if (contentInput.value.trim().length > 500) { + contentError.textContent = 'Message cannot exceed 500 characters'; + contentInput.classList.add('error-field'); + isValid = false; + } - if (!isValid) { - e.preventDefault(); - } - }); + if (!isValid) { + e.preventDefault(); + } + }); }); diff --git a/src/js/chat-json.js b/src/js/chat-json.js index 275f252..2f421b6 100644 --- a/src/js/chat-json.js +++ b/src/js/chat-json.js @@ -27,19 +27,6 @@ let hasMoreMessages = true; // UTILITY FUNCTIONS // ============================================================================ -/** - * Escapes HTML special characters to prevent XSS attacks - * This is important when displaying user-generated content! - * - * @param {string} text - The text to escape - * @returns {string} - The escaped text safe for HTML display - */ -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - /** * Shorthand function to select a single DOM element * Similar to jQuery's $() but returns only the first match @@ -48,7 +35,7 @@ function escapeHtml(text) { * @returns {Element|null} - The matched element or null */ function $(selector) { - return document.querySelector(selector); + return document.querySelector(selector); } /** @@ -59,7 +46,7 @@ function $(selector) { * @returns {NodeList} - List of matched elements */ function $$(selector) { - return document.querySelectorAll(selector); + return document.querySelectorAll(selector); } /** @@ -69,7 +56,7 @@ function $$(selector) { * @returns {string} - The CSRF token value */ function getCsrfToken() { - return $('meta[name="csrf-token"]').getAttribute('content'); + return $('meta[name="csrf-token"]').getAttribute('content'); } // ============================================================================ @@ -81,9 +68,9 @@ function getCsrfToken() { * Called when submitting a message to provide visual feedback */ function showLoading() { - $('#contentLoading').style.display = 'block'; - $('#txt').style.display = 'none'; - $('#author').style.display = 'none'; + $('#contentLoading').style.display = 'block'; + $('#txt').style.display = 'none'; + $('#author').style.display = 'none'; } /** @@ -91,19 +78,23 @@ function showLoading() { * Called after a message submission completes */ function hideLoading() { - $('#contentLoading').style.display = 'none'; - $('#txt').style.display = 'block'; - $('#author').style.display = 'block'; + $('#contentLoading').style.display = 'none'; + $('#txt').style.display = 'block'; + $('#author').style.display = 'block'; } /** * Clears all error messages and removes error styling from inputs */ function clearErrors() { - // Clear error text - $$('.error').forEach(el => el.textContent = ''); - // Remove error styling from inputs - $$('input').forEach(el => el.classList.remove('error-field')); + // Clear error text + $$('.error').forEach((el) => { + el.textContent = ''; + }); + // Remove error styling from inputs + $$('input').forEach((el) => { + el.classList.remove('error-field'); + }); } /** @@ -114,8 +105,8 @@ function clearErrors() { * @param {string} inputId - The ID of the input field to highlight */ function showError(fieldId, message, inputId) { - $(fieldId).textContent = message; - $(inputId).classList.add('error-field'); + $(fieldId).textContent = message; + $(inputId).classList.add('error-field'); } /** @@ -127,24 +118,24 @@ function showError(fieldId, message, inputId) { * @returns {DocumentFragment} - A document fragment containing the message elements */ function createMessageElement(author, message) { - const fragment = document.createDocumentFragment(); + const fragment = document.createDocumentFragment(); - // Create bold element for author name - const bold = document.createElement('b'); - bold.textContent = author; + // Create bold element for author name + const bold = document.createElement('b'); + bold.textContent = author; - // Create text node for ": " and message - const textNode = document.createTextNode(': ' + message); + // Create text node for ": " and message + const textNode = document.createTextNode(`: ${message}`); - // Create line break - const br = document.createElement('br'); + // Create line break + const br = document.createElement('br'); - // Assemble the fragment - fragment.appendChild(bold); - fragment.appendChild(textNode); - fragment.appendChild(br); + // Assemble the fragment + fragment.appendChild(bold); + fragment.appendChild(textNode); + fragment.appendChild(br); - return fragment; + return fragment; } // ============================================================================ @@ -172,46 +163,46 @@ function createMessageElement(author, message) { * @returns {number} - The number of messages added */ function addMessages(data, append = false) { - let messagesAdded = 0; - const messageWindow = $('#messagewindow'); - - // Check if we have messages in the response - if (data.messages && data.messages.length > 0) { - // Loop through each message in the array - // This is cleaner than jQuery's $.each() method! - data.messages.forEach(function(val) { - // Create message element safely (no innerHTML) - const messageFragment = createMessageElement(val.user, val.msg); - - if (append) { - // Add to the end of the message window - messageWindow.appendChild(messageFragment); - } else { - // Add to the end (for initial load, messages come in order) - messageWindow.appendChild(messageFragment); - } - messagesAdded++; - }); - } - - // Check pagination info from JSON - // JSON makes this much easier than parsing XML! - if (data.pagination) { - hasMoreMessages = data.pagination.hasNext; - } else { - hasMoreMessages = false; - } - - // Update the "Load More" button visibility - const loadMoreBtn = $('#load-more-btn'); - if (hasMoreMessages) { - loadMoreBtn.style.display = 'block'; - loadMoreBtn.textContent = 'Load More Messages'; - } else { - loadMoreBtn.style.display = 'none'; - } + let messagesAdded = 0; + const messageWindow = $('#messagewindow'); + + // Check if we have messages in the response + if (data.messages && data.messages.length > 0) { + // Loop through each message in the array + // This is cleaner than jQuery's $.each() method! + data.messages.forEach((val) => { + // Create message element safely (no innerHTML) + const messageFragment = createMessageElement(val.user, val.msg); + + if (append) { + // Add to the end of the message window + messageWindow.appendChild(messageFragment); + } else { + // Add to the end (for initial load, messages come in order) + messageWindow.appendChild(messageFragment); + } + messagesAdded++; + }); + } + + // Check pagination info from JSON + // JSON makes this much easier than parsing XML! + if (data.pagination) { + hasMoreMessages = data.pagination.hasNext; + } else { + hasMoreMessages = false; + } + + // Update the "Load More" button visibility + const loadMoreBtn = $('#load-more-btn'); + if (hasMoreMessages) { + loadMoreBtn.style.display = 'block'; + loadMoreBtn.textContent = 'Load More Messages'; + } else { + loadMoreBtn.style.display = 'none'; + } - return messagesAdded; + return messagesAdded; } // ============================================================================ @@ -227,42 +218,42 @@ function addMessages(data, append = false) { * @param {number} page - The page number to load */ function loadMsg(page = 1) { - // Build the URL with query parameters for pagination - const url = `${CHAT_ROUTES.messagesApi}?page=${page}&per_page=${messagesPerPage}`; - - // Use fetch() to make a GET request - // fetch() returns a Promise that resolves to a Response object - fetch(url, { - method: 'GET', - headers: { - // Include CSRF token for security - 'X-CSRF-TOKEN': getCsrfToken(), - // Tell the server we expect JSON back - 'Accept': 'application/json' - } + // Build the URL with query parameters for pagination + const url = `${CHAT_ROUTES.messagesApi}?page=${page}&per_page=${messagesPerPage}`; + + // Use fetch() to make a GET request + // fetch() returns a Promise that resolves to a Response object + fetch(url, { + method: 'GET', + headers: { + // Include CSRF token for security + 'X-CSRF-TOKEN': getCsrfToken(), + // Tell the server we expect JSON back + Accept: 'application/json', + }, + }) + .then((response) => { + // Check if the request was successful + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // Parse the JSON response automatically + // This is much simpler than XML parsing! + return response.json(); }) - .then(response => { - // Check if the request was successful - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Parse the JSON response automatically - // This is much simpler than XML parsing! - return response.json(); - }) - .then(data => { - // Remove the "Loading..." text - const loading = $('#loading'); - if (loading) { - loading.remove(); - } - - // Add the messages to the chat window - addMessages(data); + .then((data) => { + // Remove the "Loading..." text + const loading = $('#loading'); + if (loading) { + loading.remove(); + } + + // Add the messages to the chat window + addMessages(data); }) - .catch(error => { - // Handle any errors that occurred during the fetch - console.error('Error loading messages:', error); + .catch((error) => { + // Handle any errors that occurred during the fetch + console.error('Error loading messages:', error); }); } @@ -272,37 +263,37 @@ function loadMsg(page = 1) { * @param {number} page - The page number to load */ function loadOlderMessages(page) { - const url = `${CHAT_ROUTES.messagesApi}?page=${page}&per_page=${messagesPerPage}`; - const loadMoreBtn = $('#load-more-btn'); - - fetch(url, { - method: 'GET', - headers: { - 'X-CSRF-TOKEN': getCsrfToken(), - 'Accept': 'application/json' - } - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); + const url = `${CHAT_ROUTES.messagesApi}?page=${page}&per_page=${messagesPerPage}`; + const loadMoreBtn = $('#load-more-btn'); + + fetch(url, { + method: 'GET', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + Accept: 'application/json', + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); }) - .then(data => { - const messagesAdded = addMessages(data, true); - - if (messagesAdded === 0) { - loadMoreBtn.textContent = 'No more messages'; - loadMoreBtn.disabled = true; - } else { - loadMoreBtn.textContent = 'Load More Messages'; - } + .then((data) => { + const messagesAdded = addMessages(data, true); + + if (messagesAdded === 0) { + loadMoreBtn.textContent = 'No more messages'; + loadMoreBtn.disabled = true; + } else { + loadMoreBtn.textContent = 'Load More Messages'; + } }) - .catch(error => { - // If loading fails, revert the page counter and show error - currentPage--; - loadMoreBtn.textContent = 'Failed to load. Try again.'; - console.error('Error loading older messages:', error); + .catch((error) => { + // If loading fails, revert the page counter and show error + currentPage--; + loadMoreBtn.textContent = 'Failed to load. Try again.'; + console.error('Error loading older messages:', error); }); } @@ -316,66 +307,66 @@ function loadOlderMessages(page) { * @param {string} message - The message content to send */ function postMessage(name, message) { - showLoading(); - - // Create the form data to send - // URLSearchParams creates application/x-www-form-urlencoded data - const formData = new URLSearchParams(); - formData.append('name', name); - formData.append('message', message); - formData.append('action', 'postmsg'); - formData.append(CSRF_TOKEN_NAME, getCsrfToken()); - - // Make a POST request with fetch() - fetch(CHAT_ROUTES.update, { - method: 'POST', - headers: { - // This header tells the server we're sending form data - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json' - }, - body: formData.toString() + showLoading(); + + // Create the form data to send + // URLSearchParams creates application/x-www-form-urlencoded data + const formData = new URLSearchParams(); + formData.append('name', name); + formData.append('message', message); + formData.append('action', 'postmsg'); + formData.append(CSRF_TOKEN_NAME, getCsrfToken()); + + // Make a POST request with fetch() + fetch(CHAT_ROUTES.update, { + method: 'POST', + headers: { + // This header tells the server we're sending form data + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: formData.toString(), + }) + .then(async (response) => { + const data = await response.json(); + if (!response.ok && !data.error) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return data; }) - .then(async response => { - const data = await response.json(); - if (!response.ok && !data.error) { - throw new Error(`HTTP error! status: ${response.status}`); + .then((data) => { + hideLoading(); + + if (data?.error) { + // Display server-side validation errors + if (data.error.details?.name) { + showError('#name-error', data.error.details.name, '#name'); } - return data; - }) - .then(data => { - hideLoading(); - - if (data && data.error) { - // Display server-side validation errors - if (data.error.details && data.error.details.name) { - showError('#name-error', data.error.details.name, '#name'); - } - if (data.error.details && data.error.details.message) { - showError('#content-error', data.error.details.message, '#content'); - } - if (!data.error.details?.name && !data.error.details?.message) { - alert(`${data.error.message} (reference: ${data.error.correlation_id})`); - } - } else { - // Success! Add the message to the chat window - const messageWindow = $('#messagewindow'); - const messageFragment = createMessageElement(name, message); - - // prepend = add to the beginning (newest messages first) - messageWindow.insertBefore(messageFragment, messageWindow.firstChild); - - // Clear the message field and refocus it - // Note: We keep the name field filled for convenience - const contentInput = $('#content'); - contentInput.value = ''; - contentInput.focus(); + if (data.error.details?.message) { + showError('#content-error', data.error.details.message, '#content'); + } + if (!data.error.details?.name && !data.error.details?.message) { + alert(`${data.error.message} (reference: ${data.error.correlation_id})`); } + } else { + // Success! Add the message to the chat window + const messageWindow = $('#messagewindow'); + const messageFragment = createMessageElement(name, message); + + // prepend = add to the beginning (newest messages first) + messageWindow.insertBefore(messageFragment, messageWindow.firstChild); + + // Clear the message field and refocus it + // Note: We keep the name field filled for convenience + const contentInput = $('#content'); + contentInput.value = ''; + contentInput.focus(); + } }) - .catch(error => { - hideLoading(); - alert('An error occurred while sending your message. Please try again.'); - console.error('Error posting message:', error); + .catch((error) => { + hideLoading(); + alert('An error occurred while sending your message. Please try again.'); + console.error('Error posting message:', error); }); } @@ -390,31 +381,31 @@ function postMessage(name, message) { * @returns {boolean} - True if valid, false otherwise */ function validateName(name) { - if (name === '') { - showError('#name-error', 'Name is required', '#name'); - return false; - } - - if (name.length < 2) { - showError('#name-error', 'Name must be at least 2 characters long', '#name'); - return false; - } - - if (name.length > 50) { - showError('#name-error', 'Name cannot exceed 50 characters', '#name'); - return false; - } - - // Regular expression to check for alphanumeric characters and spaces only - // ^ = start of string - // [a-zA-Z0-9 ]+ = one or more letters, numbers, or spaces - // $ = end of string - if (!/^[a-zA-Z0-9 ]+$/.test(name)) { - showError('#name-error', 'Name can only contain alphanumeric characters and spaces', '#name'); - return false; - } - - return true; + if (name === '') { + showError('#name-error', 'Name is required', '#name'); + return false; + } + + if (name.length < 2) { + showError('#name-error', 'Name must be at least 2 characters long', '#name'); + return false; + } + + if (name.length > 50) { + showError('#name-error', 'Name cannot exceed 50 characters', '#name'); + return false; + } + + // Regular expression to check for alphanumeric characters and spaces only + // ^ = start of string + // [a-zA-Z0-9 ]+ = one or more letters, numbers, or spaces + // $ = end of string + if (!/^[a-zA-Z0-9 ]+$/.test(name)) { + showError('#name-error', 'Name can only contain alphanumeric characters and spaces', '#name'); + return false; + } + + return true; } /** @@ -424,17 +415,17 @@ function validateName(name) { * @returns {boolean} - True if valid, false otherwise */ function validateMessage(message) { - if (message === '') { - showError('#content-error', 'Message is required', '#content'); - return false; - } + if (message === '') { + showError('#content-error', 'Message is required', '#content'); + return false; + } - if (message.length > 500) { - showError('#content-error', 'Message cannot exceed 500 characters', '#content'); - return false; - } + if (message.length > 500) { + showError('#content-error', 'Message cannot exceed 500 characters', '#content'); + return false; + } - return true; + return true; } // ============================================================================ @@ -447,52 +438,52 @@ function validateMessage(message) { * DOMContentLoaded is similar to jQuery's $(document).ready() * It fires when the HTML is parsed, before images and stylesheets are loaded */ -document.addEventListener('DOMContentLoaded', function() { - // Load initial messages - loadMsg(currentPage); - hideLoading(); - - // Create and add the "Load More" button - const messageWindow = $('#messagewindow'); - const loadMoreContainer = document.createElement('div'); - loadMoreContainer.id = 'load-more-container'; - - const loadMoreBtn = document.createElement('button'); - loadMoreBtn.id = 'load-more-btn'; - loadMoreBtn.style.display = 'none'; - loadMoreBtn.textContent = 'Load More Messages'; - loadMoreContainer.appendChild(loadMoreBtn); - - messageWindow.after(loadMoreContainer); - - // Add click handler for the "Load More" button - $('#load-more-btn').addEventListener('click', function() { - if (hasMoreMessages) { - currentPage++; - this.textContent = 'Loading...'; - loadOlderMessages(currentPage); - } - }); +document.addEventListener('DOMContentLoaded', () => { + // Load initial messages + loadMsg(currentPage); + hideLoading(); + + // Create and add the "Load More" button + const messageWindow = $('#messagewindow'); + const loadMoreContainer = document.createElement('div'); + loadMoreContainer.id = 'load-more-container'; + + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.id = 'load-more-btn'; + loadMoreBtn.style.display = 'none'; + loadMoreBtn.textContent = 'Load More Messages'; + loadMoreContainer.appendChild(loadMoreBtn); + + messageWindow.after(loadMoreContainer); + + // Add click handler for the "Load More" button + $('#load-more-btn').addEventListener('click', function () { + if (hasMoreMessages) { + currentPage++; + this.textContent = 'Loading...'; + loadOlderMessages(currentPage); + } + }); - // Handle form submission - $('form#chatform').addEventListener('submit', function(event) { - // Prevent the default form submission (page reload) - event.preventDefault(); + // Handle form submission + $('form#chatform').addEventListener('submit', (event) => { + // Prevent the default form submission (page reload) + event.preventDefault(); - // Clear any previous errors - clearErrors(); + // Clear any previous errors + clearErrors(); - // Get and trim the form values - const name = $('#name').value.trim(); - const message = $('#content').value.trim(); + // Get and trim the form values + const name = $('#name').value.trim(); + const message = $('#content').value.trim(); - // Validate both fields - we want to show all errors at once - const isNameValid = validateName(name); - const isMessageValid = validateMessage(message); + // Validate both fields - we want to show all errors at once + const isNameValid = validateName(name); + const isMessageValid = validateMessage(message); - // Only submit if both fields are valid - if (isNameValid && isMessageValid) { - postMessage(name, message); - } - }); + // Only submit if both fields are valid + if (isNameValid && isMessageValid) { + postMessage(name, message); + } + }); }); diff --git a/src/js/chat.js b/src/js/chat.js index 34e557f..e2eb649 100644 --- a/src/js/chat.js +++ b/src/js/chat.js @@ -26,19 +26,6 @@ let hasMoreMessages = true; // UTILITY FUNCTIONS // ============================================================================ -/** - * Escapes HTML special characters to prevent XSS attacks - * This is important when displaying user-generated content! - * - * @param {string} text - The text to escape - * @returns {string} - The escaped text safe for HTML display - */ -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - /** * Shorthand function to select a single DOM element * Similar to jQuery's $() but returns only the first match @@ -47,7 +34,7 @@ function escapeHtml(text) { * @returns {Element|null} - The matched element or null */ function $(selector) { - return document.querySelector(selector); + return document.querySelector(selector); } /** @@ -58,7 +45,7 @@ function $(selector) { * @returns {NodeList} - List of matched elements */ function $$(selector) { - return document.querySelectorAll(selector); + return document.querySelectorAll(selector); } /** @@ -68,7 +55,7 @@ function $$(selector) { * @returns {string} - The CSRF token value */ function getCsrfToken() { - return $('meta[name="csrf-token"]').getAttribute('content'); + return $('meta[name="csrf-token"]').getAttribute('content'); } // ============================================================================ @@ -80,8 +67,8 @@ function getCsrfToken() { * Called when submitting a message to provide visual feedback */ function showLoading() { - $('#contentLoading').style.display = 'block'; - $('#txt').style.display = 'none'; + $('#contentLoading').style.display = 'block'; + $('#txt').style.display = 'none'; } /** @@ -89,18 +76,22 @@ function showLoading() { * Called after a message submission completes */ function hideLoading() { - $('#contentLoading').style.display = 'none'; - $('#txt').style.display = 'block'; + $('#contentLoading').style.display = 'none'; + $('#txt').style.display = 'block'; } /** * Clears all error messages and removes error styling from inputs */ function clearErrors() { - // Clear error text - $$('.error').forEach(el => el.textContent = ''); - // Remove error styling from inputs - $$('input').forEach(el => el.classList.remove('error-field')); + // Clear error text + $$('.error').forEach((el) => { + el.textContent = ''; + }); + // Remove error styling from inputs + $$('input').forEach((el) => { + el.classList.remove('error-field'); + }); } /** @@ -111,8 +102,8 @@ function clearErrors() { * @param {string} inputId - The ID of the input field to highlight */ function showError(fieldId, message, inputId) { - $(fieldId).textContent = message; - $(inputId).classList.add('error-field'); + $(fieldId).textContent = message; + $(inputId).classList.add('error-field'); } /** @@ -124,24 +115,24 @@ function showError(fieldId, message, inputId) { * @returns {DocumentFragment} - A document fragment containing the message elements */ function createMessageElement(author, message) { - const fragment = document.createDocumentFragment(); + const fragment = document.createDocumentFragment(); - // Create bold element for author name - const bold = document.createElement('b'); - bold.textContent = author; + // Create bold element for author name + const bold = document.createElement('b'); + bold.textContent = author; - // Create text node for ": " and message - const textNode = document.createTextNode(': ' + message); + // Create text node for ": " and message + const textNode = document.createTextNode(`: ${message}`); - // Create line break - const br = document.createElement('br'); + // Create line break + const br = document.createElement('br'); - // Assemble the fragment - fragment.appendChild(bold); - fragment.appendChild(textNode); - fragment.appendChild(br); + // Assemble the fragment + fragment.appendChild(bold); + fragment.appendChild(textNode); + fragment.appendChild(br); - return fragment; + return fragment; } // ============================================================================ @@ -167,45 +158,45 @@ function createMessageElement(author, message) { * @returns {number} - The number of messages added */ function addMessages(xml, append = false) { - let messagesAdded = 0; - const messageWindow = $('#messagewindow'); - - // Find all elements in the XML - const messages = xml.querySelectorAll('message'); - - messages.forEach(message => { - // Extract author and text from the XML structure - const author = message.querySelector('author').textContent; - const msg = message.querySelector('text').textContent; - - // Create message element safely (no innerHTML) - const messageFragment = createMessageElement(author, msg); - - if (append) { - // Add to the end of the message window - messageWindow.appendChild(messageFragment); - } else { - // Add to the end (for initial load, messages come in order) - messageWindow.appendChild(messageFragment); - } - messagesAdded++; - }); + let messagesAdded = 0; + const messageWindow = $('#messagewindow'); - // Check pagination info from XML - const totalPagesEl = xml.querySelector('pagination totalPages'); - const totalPages = totalPagesEl ? parseInt(totalPagesEl.textContent) : 0; - hasMoreMessages = currentPage < totalPages; + // Find all elements in the XML + const messages = xml.querySelectorAll('message'); - // Update the "Load More" button visibility - const loadMoreBtn = $('#load-more-btn'); - if (hasMoreMessages) { - loadMoreBtn.style.display = 'block'; - loadMoreBtn.textContent = 'Load More Messages'; + messages.forEach((message) => { + // Extract author and text from the XML structure + const author = message.querySelector('author').textContent; + const msg = message.querySelector('text').textContent; + + // Create message element safely (no innerHTML) + const messageFragment = createMessageElement(author, msg); + + if (append) { + // Add to the end of the message window + messageWindow.appendChild(messageFragment); } else { - loadMoreBtn.style.display = 'none'; + // Add to the end (for initial load, messages come in order) + messageWindow.appendChild(messageFragment); } + messagesAdded++; + }); + + // Check pagination info from XML + const totalPagesEl = xml.querySelector('pagination totalPages'); + const totalPages = totalPagesEl ? parseInt(totalPagesEl.textContent, 10) : 0; + hasMoreMessages = currentPage < totalPages; + + // Update the "Load More" button visibility + const loadMoreBtn = $('#load-more-btn'); + if (hasMoreMessages) { + loadMoreBtn.style.display = 'block'; + loadMoreBtn.textContent = 'Load More Messages'; + } else { + loadMoreBtn.style.display = 'none'; + } - return messagesAdded; + return messagesAdded; } // ============================================================================ @@ -221,43 +212,43 @@ function addMessages(xml, append = false) { * @param {number} page - The page number to load */ function loadMsg(page = 1) { - // Build the URL with query parameters for pagination - const url = `${CHAT_ROUTES.backend}?page=${page}&per_page=${messagesPerPage}`; - - // Use fetch() to make a GET request - // fetch() returns a Promise that resolves to a Response object - fetch(url, { - method: 'GET', - headers: { - // Include CSRF token for security - 'X-CSRF-TOKEN': getCsrfToken() - } - }) - .then(response => { - // Check if the request was successful - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // For XML responses, we get the raw text first - return response.text(); + // Build the URL with query parameters for pagination + const url = `${CHAT_ROUTES.backend}?page=${page}&per_page=${messagesPerPage}`; + + // Use fetch() to make a GET request + // fetch() returns a Promise that resolves to a Response object + fetch(url, { + method: 'GET', + headers: { + // Include CSRF token for security + 'X-CSRF-TOKEN': getCsrfToken(), + }, + }) + .then((response) => { + // Check if the request was successful + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // For XML responses, we get the raw text first + return response.text(); }) - .then(xmlText => { - // Parse the XML text into a Document object - const parser = new DOMParser(); - const xml = parser.parseFromString(xmlText, 'text/xml'); - - // Remove the "Loading..." text - const loading = $('#loading'); - if (loading) { - loading.remove(); - } - - // Add the messages to the chat window - addMessages(xml); + .then((xmlText) => { + // Parse the XML text into a Document object + const parser = new DOMParser(); + const xml = parser.parseFromString(xmlText, 'text/xml'); + + // Remove the "Loading..." text + const loading = $('#loading'); + if (loading) { + loading.remove(); + } + + // Add the messages to the chat window + addMessages(xml); }) - .catch(error => { - // Handle any errors that occurred during the fetch - console.error('Error loading messages:', error); + .catch((error) => { + // Handle any errors that occurred during the fetch + console.error('Error loading messages:', error); }); } @@ -267,39 +258,39 @@ function loadMsg(page = 1) { * @param {number} page - The page number to load */ function loadOlderMessages(page) { - const url = `${CHAT_ROUTES.backend}?page=${page}&per_page=${messagesPerPage}`; - const loadMoreBtn = $('#load-more-btn'); - - fetch(url, { - method: 'GET', - headers: { - 'X-CSRF-TOKEN': getCsrfToken() - } + const url = `${CHAT_ROUTES.backend}?page=${page}&per_page=${messagesPerPage}`; + const loadMoreBtn = $('#load-more-btn'); + + fetch(url, { + method: 'GET', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.text(); }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.text(); - }) - .then(xmlText => { - const parser = new DOMParser(); - const xml = parser.parseFromString(xmlText, 'text/xml'); + .then((xmlText) => { + const parser = new DOMParser(); + const xml = parser.parseFromString(xmlText, 'text/xml'); - const messagesAdded = addMessages(xml, true); + const messagesAdded = addMessages(xml, true); - if (messagesAdded === 0) { - loadMoreBtn.textContent = 'No more messages'; - loadMoreBtn.disabled = true; - } else { - loadMoreBtn.textContent = 'Load More Messages'; - } + if (messagesAdded === 0) { + loadMoreBtn.textContent = 'No more messages'; + loadMoreBtn.disabled = true; + } else { + loadMoreBtn.textContent = 'Load More Messages'; + } }) - .catch(error => { - // If loading fails, revert the page counter and show error - currentPage--; - loadMoreBtn.textContent = 'Failed to load. Try again.'; - console.error('Error loading older messages:', error); + .catch((error) => { + // If loading fails, revert the page counter and show error + currentPage--; + loadMoreBtn.textContent = 'Failed to load. Try again.'; + console.error('Error loading older messages:', error); }); } @@ -312,58 +303,58 @@ function loadOlderMessages(page) { * @param {string} message - The message content to send */ function postMessage(message) { - showLoading(); - - // Create the form data to send - // URLSearchParams creates application/x-www-form-urlencoded data - const formData = new URLSearchParams(); - formData.append('message', message); - formData.append('action', 'postmsg'); - formData.append(CSRF_TOKEN_NAME, getCsrfToken()); - - // Make a POST request with fetch() - fetch(CHAT_ROUTES.update, { - method: 'POST', - headers: { - // This header tells the server we're sending form data - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json' - }, - body: formData.toString() - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // The server returns JSON for the response - return response.json(); + showLoading(); + + // Create the form data to send + // URLSearchParams creates application/x-www-form-urlencoded data + const formData = new URLSearchParams(); + formData.append('message', message); + formData.append('action', 'postmsg'); + formData.append(CSRF_TOKEN_NAME, getCsrfToken()); + + // Make a POST request with fetch() + fetch(CHAT_ROUTES.update, { + method: 'POST', + headers: { + // This header tells the server we're sending form data + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: formData.toString(), + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // The server returns JSON for the response + return response.json(); }) - .then(data => { - hideLoading(); - - if (data && data.success === false) { - // Display server-side validation errors - if (data.errors && data.errors.message) { - showError('#content-error', data.errors.message, '#content'); - } - } else { - // Success! Add the message to the chat window - const messageWindow = $('#messagewindow'); - const messageFragment = createMessageElement(CURRENT_USERNAME, message); - - // prepend = add to the beginning (newest messages first) - messageWindow.insertBefore(messageFragment, messageWindow.firstChild); - - // Clear the input field and refocus it - const contentInput = $('#content'); - contentInput.value = ''; - contentInput.focus(); + .then((data) => { + hideLoading(); + + if (data && data.success === false) { + // Display server-side validation errors + if (data.errors?.message) { + showError('#content-error', data.errors.message, '#content'); } + } else { + // Success! Add the message to the chat window + const messageWindow = $('#messagewindow'); + const messageFragment = createMessageElement(CURRENT_USERNAME, message); + + // prepend = add to the beginning (newest messages first) + messageWindow.insertBefore(messageFragment, messageWindow.firstChild); + + // Clear the input field and refocus it + const contentInput = $('#content'); + contentInput.value = ''; + contentInput.focus(); + } }) - .catch(error => { - hideLoading(); - alert('An error occurred while sending your message. Please try again.'); - console.error('Error posting message:', error); + .catch((error) => { + hideLoading(); + alert('An error occurred while sending your message. Please try again.'); + console.error('Error posting message:', error); }); } @@ -378,17 +369,17 @@ function postMessage(message) { * @returns {boolean} - True if valid, false otherwise */ function validateMessage(message) { - if (message === '') { - showError('#content-error', 'Message is required', '#content'); - return false; - } + if (message === '') { + showError('#content-error', 'Message is required', '#content'); + return false; + } - if (message.length > 500) { - showError('#content-error', 'Message cannot exceed 500 characters', '#content'); - return false; - } + if (message.length > 500) { + showError('#content-error', 'Message cannot exceed 500 characters', '#content'); + return false; + } - return true; + return true; } // ============================================================================ @@ -401,47 +392,47 @@ function validateMessage(message) { * DOMContentLoaded is similar to jQuery's $(document).ready() * It fires when the HTML is parsed, before images and stylesheets are loaded */ -document.addEventListener('DOMContentLoaded', function() { - // Load initial messages - loadMsg(currentPage); - hideLoading(); - - // Create and add the "Load More" button - const messageWindow = $('#messagewindow'); - const loadMoreContainer = document.createElement('div'); - loadMoreContainer.id = 'load-more-container'; - - const loadMoreBtn = document.createElement('button'); - loadMoreBtn.id = 'load-more-btn'; - loadMoreBtn.style.display = 'none'; - loadMoreBtn.textContent = 'Load More Messages'; - loadMoreContainer.appendChild(loadMoreBtn); - - messageWindow.after(loadMoreContainer); - - // Add click handler for the "Load More" button - $('#load-more-btn').addEventListener('click', function() { - if (hasMoreMessages) { - currentPage++; - this.textContent = 'Loading...'; - loadOlderMessages(currentPage); - } - }); +document.addEventListener('DOMContentLoaded', () => { + // Load initial messages + loadMsg(currentPage); + hideLoading(); + + // Create and add the "Load More" button + const messageWindow = $('#messagewindow'); + const loadMoreContainer = document.createElement('div'); + loadMoreContainer.id = 'load-more-container'; + + const loadMoreBtn = document.createElement('button'); + loadMoreBtn.id = 'load-more-btn'; + loadMoreBtn.style.display = 'none'; + loadMoreBtn.textContent = 'Load More Messages'; + loadMoreContainer.appendChild(loadMoreBtn); + + messageWindow.after(loadMoreContainer); + + // Add click handler for the "Load More" button + $('#load-more-btn').addEventListener('click', function () { + if (hasMoreMessages) { + currentPage++; + this.textContent = 'Loading...'; + loadOlderMessages(currentPage); + } + }); - // Handle form submission - $('form#chatform').addEventListener('submit', function(event) { - // Prevent the default form submission (page reload) - event.preventDefault(); + // Handle form submission + $('form#chatform').addEventListener('submit', (event) => { + // Prevent the default form submission (page reload) + event.preventDefault(); - // Clear any previous errors - clearErrors(); + // Clear any previous errors + clearErrors(); - // Get and trim the message value - const message = $('#content').value.trim(); + // Get and trim the message value + const message = $('#content').value.trim(); - // Validate and send if valid - if (validateMessage(message)) { - postMessage(message); - } - }); + // Validate and send if valid + if (validateMessage(message)) { + postMessage(message); + } + }); }); diff --git a/src/svelte/App.svelte b/src/svelte/App.svelte index 5746459..1f438e9 100644 --- a/src/svelte/App.svelte +++ b/src/svelte/App.svelte @@ -37,7 +37,7 @@ --> @@ -626,9 +632,7 @@
@@ -659,9 +663,7 @@ {#if messages.length === 0} -
- No messages yet. Be the first to send a message! -
+
No messages yet. Be the first to send a message!
{/if} {/if} @@ -670,11 +672,7 @@ {#if !loading && hasMoreMessages}
- - +
diff --git a/src/svelte/main.js b/src/svelte/main.js index 7ea730d..1319949 100644 --- a/src/svelte/main.js +++ b/src/svelte/main.js @@ -35,21 +35,21 @@ import { mount } from 'svelte'; * without making additional API calls. */ const config = { - // API routes for chat operations - chatRoutes: { - update: window.CHAT_ROUTES?.update || '/api/v1/messages', - api: window.CHAT_ROUTES?.messagesApi || '/api/v1/messages' - }, + // API routes for chat operations + chatRoutes: { + update: window.CHAT_ROUTES?.update || '/api/v1/messages', + api: window.CHAT_ROUTES?.messagesApi || '/api/v1/messages', + }, - // CSRF token name for secure form submissions - csrfTokenName: window.CSRF_TOKEN_NAME || 'csrf_test_name', + // CSRF token name for secure form submissions + csrfTokenName: window.CSRF_TOKEN_NAME || 'csrf_test_name', - // Current user information from session - username: window.CURRENT_USERNAME || 'Guest', - userId: window.CURRENT_USER_ID || 0, + // Current user information from session + username: window.CURRENT_USERNAME || 'Guest', + userId: window.CURRENT_USER_ID || 0, - // WebSocket authentication token (generated on login) - wsToken: window.WEBSOCKET_TOKEN || '' + // WebSocket authentication token (generated on login) + wsToken: window.WEBSOCKET_TOKEN || '', }; /** @@ -67,14 +67,14 @@ const config = { * inside the App component via $props(). */ const app = mount(App, { - // The DOM element where the app will be rendered - target: document.getElementById('app'), + // The DOM element where the app will be rendered + target: document.getElementById('app'), - // Props passed to the root component - // These are accessed in App.svelte using $props() - props: { - config - } + // Props passed to the root component + // These are accessed in App.svelte using $props() + props: { + config, + }, }); // Export the app instance for debugging (optional) diff --git a/src/vue/App.vue b/src/vue/App.vue index 05c59f8..01f4ebf 100644 --- a/src/vue/App.vue +++ b/src/vue/App.vue @@ -3,9 +3,7 @@
@@ -22,18 +20,12 @@
-
- No messages yet. Be the first to send a message! -
+
No messages yet. Be the first to send a message!
- @@ -43,10 +35,10 @@
-