From 47e3cbafb7a1792a3b1aa840daee49ca7674915a Mon Sep 17 00:00:00 2001 From: BOTSU Date: Thu, 6 Aug 2026 21:11:30 +0200 Subject: [PATCH] feat(wikipedia): Wikipedia reader with search and article view --- apps/client/package.json | 2 +- apps/client/src/app/pages/Router.tsx | 5 + apps/client/src/app/pages/paths.ts | 2 + apps/client/src/botsu/apps/catalog.test.ts | 13 +- apps/client/src/botsu/apps/catalog.ts | 101 +++++- apps/client/src/botsu/shell/BotsuLinks.tsx | 30 +- apps/client/src/botsu/shell/BotsuNav.tsx | 86 ++++- apps/client/src/botsu/shell/layout.test.ts | 20 +- apps/client/src/botsu/shell/shell.css | 7 + .../botsu/wikipedia/BotsuWikipediaArticle.tsx | 109 ++++++ .../botsu/wikipedia/BotsuWikipediaHome.tsx | 133 +++++++ apps/client/src/botsu/wikipedia/index.ts | 4 + apps/client/src/botsu/wikipedia/model.test.ts | 158 ++++++++ apps/client/src/botsu/wikipedia/model.ts | 172 +++++++++ .../botsu/wikipedia/wikipedia-scraper.test.ts | 110 ++++++ .../src/botsu/wikipedia/wikipedia-scraper.ts | 139 ++++++++ apps/client/src/botsu/wikipedia/wikipedia.css | 337 ++++++++++++++++++ 17 files changed, 1402 insertions(+), 26 deletions(-) create mode 100644 apps/client/src/botsu/wikipedia/BotsuWikipediaArticle.tsx create mode 100644 apps/client/src/botsu/wikipedia/BotsuWikipediaHome.tsx create mode 100644 apps/client/src/botsu/wikipedia/index.ts create mode 100644 apps/client/src/botsu/wikipedia/model.test.ts create mode 100644 apps/client/src/botsu/wikipedia/model.ts create mode 100644 apps/client/src/botsu/wikipedia/wikipedia-scraper.test.ts create mode 100644 apps/client/src/botsu/wikipedia/wikipedia-scraper.ts create mode 100644 apps/client/src/botsu/wikipedia/wikipedia.css diff --git a/apps/client/package.json b/apps/client/package.json index 7bef3c7..a8f96d8 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -16,7 +16,7 @@ "check:prettier": "prettier --check .", "fix:prettier": "prettier --write .", "typecheck": "tsc --noEmit", - "test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/documents/model.test.ts src/botsu/documents/document-share.test.ts src/botsu/documents/document-sync-bridge.test.ts src/botsu/documents/editor.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/compact-presence.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/location-publisher.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts src/botsu/shell/layout.test.ts src/botsu/start/background-options.test.ts src/botsu/start/cookie-controller.test.ts src/botsu/start/model.test.ts src/botsu/start/pixel-canvas-bridge.test.ts src/botsu/start/pixel-canvas-publisher.test.ts src/botsu/workspace/editor.test.ts src/botsu/workspace/permissions.test.ts src/botsu/workspace/state.test.ts", + "test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/documents/model.test.ts src/botsu/documents/document-share.test.ts src/botsu/documents/document-sync-bridge.test.ts src/botsu/documents/editor.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/compact-presence.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/location-publisher.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts src/botsu/shell/layout.test.ts src/botsu/start/background-options.test.ts src/botsu/start/cookie-controller.test.ts src/botsu/start/model.test.ts src/botsu/start/pixel-canvas-bridge.test.ts src/botsu/start/pixel-canvas-publisher.test.ts src/botsu/wikipedia/model.test.ts src/botsu/wikipedia/wikipedia-scraper.test.ts src/botsu/workspace/editor.test.ts src/botsu/workspace/permissions.test.ts src/botsu/workspace/state.test.ts", "typecheck:botsu": "../../node_modules/.bin/tsc -p tsconfig.botsu.json && ../../node_modules/.bin/tsc -p tsconfig.botsu-ui.json", "prepare": "husky install", "commit": "git-cz", diff --git a/apps/client/src/app/pages/Router.tsx b/apps/client/src/app/pages/Router.tsx index b6494d6..f984082 100644 --- a/apps/client/src/app/pages/Router.tsx +++ b/apps/client/src/app/pages/Router.tsx @@ -36,6 +36,8 @@ import { _BOTSU_DOCUMENTS_PATH, _BOTSU_SERVICES_PATH, _BOTSU_TCG_PATH, + _BOTSU_WIKIPEDIA_PATH, + _BOTSU_WIKIPEDIA_ARTICLE_PATH, } from './paths'; import { getAppPathFromHref, @@ -82,6 +84,7 @@ import { BotsuDocumentEditor, BotsuDocuments } from '../../botsu/documents'; import { DocumentSyncProvider } from '../../botsu/documents/DocumentSyncContext'; import { BotsuCommunityCanvasHome, BotsuStartPage } from '../../botsu/start'; import { BotsuTcgPage } from '../../botsu/tcg/BotsuTcgPage'; +import { BotsuWikipediaHome, BotsuWikipediaArticle } from '../../botsu/wikipedia'; export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => { const { hashRouter } = clientConfig; @@ -187,6 +190,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) } /> } /> } /> + } /> + } /> } /> } /> diff --git a/apps/client/src/app/pages/paths.ts b/apps/client/src/app/pages/paths.ts index e44b929..8a5d169 100644 --- a/apps/client/src/app/pages/paths.ts +++ b/apps/client/src/app/pages/paths.ts @@ -82,6 +82,8 @@ export const _BOTSU_EMBED_PATH = 'embed/:appId/'; export const _BOTSU_DOCUMENTS_PATH = 'documents/'; export const _BOTSU_DOCUMENT_PATH = 'documents/:documentId/'; export const _BOTSU_TCG_PATH = 'tcg/'; +export const _BOTSU_WIKIPEDIA_PATH = 'wikipedia/'; +export const _BOTSU_WIKIPEDIA_ARTICLE_PATH = 'wikipedia/:slug/'; export const BOTSU_PATH = '/botsu/'; export const BOTSU_APPS_PATH = `/botsu/${_BOTSU_APPS_PATH}`; export const BOTSU_SERVICES_PATH = `/botsu/${_BOTSU_SERVICES_PATH}`; diff --git a/apps/client/src/botsu/apps/catalog.test.ts b/apps/client/src/botsu/apps/catalog.test.ts index c4bc2b9..1dcb129 100644 --- a/apps/client/src/botsu/apps/catalog.test.ts +++ b/apps/client/src/botsu/apps/catalog.test.ts @@ -8,7 +8,7 @@ test('member catalogue exposes the collaborative suite without administration', assert.deepEqual( apps.map((app) => app.id), - ['discussions', 'documents', 'tables', 'files', 'transfers', 'services', 'generations-audiovisuel', 'generations-vision', 'generations-textuel', 'emoji-tcg'] + ['discussions', 'documents', 'tables', 'files', 'transfers', 'services', 'generations-audiovisuel', 'generations-vision', 'generations-textuel', 'emoji-tcg', 'radio', 'shop', 'voxel', 'dessin', 'cinema', 'casino', 'bourse', 'peertube', 'wikipedia', 'pets', 'openstreetmap'] ); assert.equal( apps.some((app) => app.id === 'administration'), @@ -40,5 +40,16 @@ test('catalogue records live and planned applications explicitly', () => { assert.equal(getApp('documents')?.availability, 'planned'); assert.equal(getApp('tables')?.launch.mode, 'iframe'); assert.equal(getApp('transfers')?.launch.mode, 'external'); + assert.equal(getApp('radio')?.availability, 'planned'); + assert.equal(getApp('shop')?.availability, 'planned'); + assert.equal(getApp('voxel')?.availability, 'planned'); + assert.equal(getApp('dessin')?.availability, 'planned'); + assert.equal(getApp('cinema')?.availability, 'planned'); + assert.equal(getApp('casino')?.availability, 'planned'); + assert.equal(getApp('bourse')?.availability, 'planned'); + assert.equal(getApp('peertube')?.availability, 'planned'); + assert.equal(getApp('wikipedia')?.availability, 'available'); + assert.equal(getApp('pets')?.availability, 'planned'); + assert.equal(getApp('openstreetmap')?.availability, 'planned'); assert.equal(getApp('missing'), undefined); }); diff --git a/apps/client/src/botsu/apps/catalog.ts b/apps/client/src/botsu/apps/catalog.ts index cd67dd0..f4ab146 100644 --- a/apps/client/src/botsu/apps/catalog.ts +++ b/apps/client/src/botsu/apps/catalog.ts @@ -10,7 +10,18 @@ export type BotsuAppId = | 'generations-audiovisuel' | 'generations-vision' | 'generations-textuel' - | 'emoji-tcg'; + | 'emoji-tcg' + | 'radio' + | 'shop' + | 'voxel' + | 'dessin' + | 'cinema' + | 'casino' + | 'bourse' + | 'peertube' + | 'wikipedia' + | 'pets' + | 'openstreetmap'; export type NativeLaunch = { mode: 'native'; path: string }; export type IframeLaunch = { mode: 'iframe'; url: string }; @@ -115,6 +126,94 @@ export const botsuApps: readonly BotsuApp[] = [ availability: 'available', launch: { mode: 'native', path: '/botsu/tcg/' }, }, + { + id: 'radio', + label: 'Radio', + description: 'Écoute de la radio en direct.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/radio/' }, + }, + { + id: 'shop', + label: 'Shop', + description: 'Boutique de la suite BOTSU.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/shop/' }, + }, + { + id: 'voxel', + label: 'Voxel', + description: 'Édition et création voxel.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/voxel/' }, + }, + { + id: 'dessin', + label: 'Dessin', + description: 'Espace de dessin collaboratif.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/dessin/' }, + }, + { + id: 'cinema', + label: 'Cinéma', + description: 'Films et séries.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/cinema/' }, + }, + { + id: 'casino', + label: 'Casino', + description: 'Jeux de hasard et divertissement.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/casino/' }, + }, + { + id: 'bourse', + label: 'Bourse', + description: 'Suivi des marchés financiers.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/bourse/' }, + }, + { + id: 'peertube', + label: 'PeerTube', + description: 'Vidéos hébergées sur le réseau PeerTube.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/peertube/' }, + }, + { + id: 'wikipedia', + label: 'Wikipédia', + description: 'Encyclopédie libre et collaborative.', + roles: ['member', 'admin'], + availability: 'available', + launch: { mode: 'native', path: '/botsu/wikipedia/' }, + }, + { + id: 'pets', + label: 'Pets', + description: 'Adopte et élève des animaux virtuels.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/pets/' }, + }, + { + id: 'openstreetmap', + label: 'OpenStreetMap', + description: 'Cartographie libre et collaborative.', + roles: ['member', 'admin'], + availability: 'planned', + launch: { mode: 'native', path: '/botsu/openstreetmap/' }, + }, ]; export const getVisibleApps = ( diff --git a/apps/client/src/botsu/shell/BotsuLinks.tsx b/apps/client/src/botsu/shell/BotsuLinks.tsx index 1c56045..0c8eefd 100644 --- a/apps/client/src/botsu/shell/BotsuLinks.tsx +++ b/apps/client/src/botsu/shell/BotsuLinks.tsx @@ -53,6 +53,34 @@ async function saveLinks( await mx.setAccountData(BOTSU_LINKS_EVENT, { links } satisfies BotsuLinksContent); } +function getFaviconUrl(url: string): string { + try { + const parsed = new URL(url); + return `https://www.google.com/s2/favicons?domain=${parsed.hostname}&sz=32`; + } catch { + return ''; + } +} + +function LinkFavicon({ url, label }: { url: string; label: string }) { + const [failed, setFailed] = useState(false); + const faviconUrl = getFaviconUrl(url); + + if (!faviconUrl || failed) { + return