From 5ca06675853d93feb5c06d8bd5792b5ef0c2fab6 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 13:57:59 -0400 Subject: [PATCH 1/8] feat: add StaticOutput component for screen reader accessibility --- src/components/StaticOutput.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/components/StaticOutput.tsx diff --git a/src/components/StaticOutput.tsx b/src/components/StaticOutput.tsx new file mode 100644 index 0000000..525a31b --- /dev/null +++ b/src/components/StaticOutput.tsx @@ -0,0 +1,11 @@ +interface StaticOutputProps { + children: React.ReactNode; +} + +export default function StaticOutput({ children }: StaticOutputProps) { + return ( + + ); +} From 53aff68973bda0787d6fe20c8059e68e61927405 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 13:58:03 -0400 Subject: [PATCH 2/8] refactor: add initialCommand support to terminal emulator --- src/components/terminal/TerminalEmulator.tsx | 17 ++++++++++++++++- src/stores/terminal-store.ts | 20 +++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/terminal/TerminalEmulator.tsx b/src/components/terminal/TerminalEmulator.tsx index 0378a93..4e881f6 100644 --- a/src/components/terminal/TerminalEmulator.tsx +++ b/src/components/terminal/TerminalEmulator.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useStore } from '@nanostores/react'; import { useTranslations } from 'next-intl'; import { useEffect, useRef, useState } from 'react'; @@ -5,11 +7,18 @@ import { $terminalHistory, $terminalHistoryVisibleIdx, $terminalPromptRef, + initializeTerminal, } from '@/stores/terminal-store'; import UnknownCmdOutput from '../cmd-outputs/UnknownCmdOutput'; import TerminalPrompt, { type TerminalPromptRef } from './TerminalPrompt'; -export default function TerminalEmulator() { +interface TerminalEmulatorProps { + initialCommand?: string; +} + +export default function TerminalEmulator({ + initialCommand = 'welcome', +}: TerminalEmulatorProps) { const history = useStore($terminalHistory); const historyVisibleIdx = useStore($terminalHistoryVisibleIdx); @@ -33,6 +42,12 @@ export default function TerminalEmulator() { } }, [hasWindow]); + useEffect(() => { + if (hasWindow) { + initializeTerminal(initialCommand); + } + }, [hasWindow, initialCommand]); + return ( hasWindow && (
diff --git a/src/stores/terminal-store.ts b/src/stores/terminal-store.ts index ed28328..8a7249c 100644 --- a/src/stores/terminal-store.ts +++ b/src/stores/terminal-store.ts @@ -1,9 +1,9 @@ -import { atom, effect, onMount } from 'nanostores'; +import { atom, effect } from 'nanostores'; import type { KeyboardEvent, RefObject } from 'react'; import type { TerminalPromptRef } from '@/components/terminal/TerminalPrompt'; import { Commands } from '@/constants/commands'; import { Key } from '@/types/keyboard'; -import { Command, type CommandEntry } from '@/types/terminal'; +import type { CommandEntry } from '@/types/terminal'; import { getPastInputStr, parseTerminalEntry } from '@/utils/terminal-utils'; export const $terminalInput = atom(''); @@ -17,15 +17,6 @@ export const $terminalPromptRef = atom | null>(null); export const $scrollY = atom(0); -const $hasGreeted = atom(false); - -onMount($terminalInput, () => { - if (!$hasGreeted.get()) { - $hasGreeted.set(true); - simulateInput(Command.Welcome); - } -}); - effect($terminalKeyEvent, (event) => { const isReadOnly = $terminalInputReadOnly.get(); if (!event || isReadOnly) return; @@ -100,6 +91,13 @@ export function simulateInput(input: string) { addChar(input); } +export function initializeTerminal(command: string) { + $terminalHistory.set([]); + $terminalHistoryIdx.set(-1); + $terminalHistoryVisibleIdx.set(0); + simulateInput(command); +} + export function setPreviousHistoryEntry() { const history = $terminalHistory.get(); const historyIdx = $terminalHistoryIdx.get(); From 88eb3fee0ae0ead7c436cfbb05eed26240404336 Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 13:58:04 -0400 Subject: [PATCH 3/8] feat: convert pages to static-first with terminal emulator --- src/app/[locale]/contact/page.tsx | 19 +++++++++++++++-- src/app/[locale]/page.tsx | 34 ++++++++++++++++++++++++++++--- src/app/[locale]/whoami/page.tsx | 19 +++++++++++++++-- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/app/[locale]/contact/page.tsx b/src/app/[locale]/contact/page.tsx index 8de5916..6983853 100644 --- a/src/app/[locale]/contact/page.tsx +++ b/src/app/[locale]/contact/page.tsx @@ -1,10 +1,25 @@ import Contact from '@/components/contact/Contact'; +import StaticOutput from '@/components/StaticOutput'; +import TerminalEmulator from '@/components/terminal/TerminalEmulator'; import type { RouteData } from '@/types/routing'; +import { Command } from '@/types/terminal'; import { setPageMeta } from '@/utils/metadata-utils'; +interface ContactPageProps { + params: Promise<{ locale: string }>; +} + export const generateMetadata = async (routeData: RouteData) => await setPageMeta(routeData, 'contact'); -export default function ContactPage() { - return ; +export default async function ContactPage({ params }: ContactPageProps) { + await params; + return ( + <> + + + + + + ); } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 8cfcc50..bf9abab 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,6 +1,34 @@ -'use client'; +import { getTranslations } from 'next-intl/server'; +import StaticOutput from '@/components/StaticOutput'; import TerminalEmulator from '@/components/terminal/TerminalEmulator'; +import { Command } from '@/types/terminal'; -export default function HomePage() { - return ; +interface HomePageProps { + params: Promise<{ locale: string }>; +} + +export default async function HomePage({ params }: HomePageProps) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: 'Terminal' }); + + return ( + <> + +
+

+ {t.rich('cmds.welcome.welcome', { + name: (name) => name, + })} +

+

{t('cmds.welcome.site_intro_1')}

+

+ {t.rich('cmds.welcome.site_intro_2', { + cmd: () => 'help', + })} +

+
+
+ + + ); } diff --git a/src/app/[locale]/whoami/page.tsx b/src/app/[locale]/whoami/page.tsx index c308f4b..bd3f146 100644 --- a/src/app/[locale]/whoami/page.tsx +++ b/src/app/[locale]/whoami/page.tsx @@ -1,10 +1,25 @@ import AboutMe from '@/components/about-me/AboutMe'; +import StaticOutput from '@/components/StaticOutput'; +import TerminalEmulator from '@/components/terminal/TerminalEmulator'; import type { RouteData } from '@/types/routing'; +import { Command } from '@/types/terminal'; import { setPageMeta } from '@/utils/metadata-utils'; +interface WhoamiPageProps { + params: Promise<{ locale: string }>; +} + export const generateMetadata = async (routeData: RouteData) => await setPageMeta(routeData, 'whoami'); -export default function WhoamiPage() { - return ; +export default async function WhoamiPage({ params }: WhoamiPageProps) { + await params; + return ( + <> + + + + + + ); } From 3bffef8f3cc36616726e5151f82c7cdb6a0414ba Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 13:58:04 -0400 Subject: [PATCH 4/8] fix: remove trailing colon from services title --- src/i18n/messages/en.json | 2 +- src/i18n/messages/fr.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 822b374..8f8e177 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -66,7 +66,7 @@ }, "services": { "description": "Display my self-hosted services status", - "title": "Services that I proudly self-host:", + "title": "Services that I proudly self-host", "connectionStatus": "Connection Status", "loading": "(Loading...)", "waitingForHeartbeats": "Waiting for heartbeats...", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index c05e888..e27fc16 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -66,7 +66,7 @@ }, "services": { "description": "Afficher l'état de mes services auto-hébergés", - "title": "Services que j'héberge avec fierté :", + "title": "Services que j'héberge avec fierté", "connectionStatus": "État de la connexion", "loading": "(Chargement...)", "waitingForHeartbeats": "En attente de heartbeats...", From 8ffaeff8e6a963ec1cbc5025f2d178333397043e Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 13:58:04 -0400 Subject: [PATCH 5/8] style: adjust ServicesOutput typography and icon --- src/components/cmd-outputs/ServicesOutput.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/cmd-outputs/ServicesOutput.tsx b/src/components/cmd-outputs/ServicesOutput.tsx index b25e7fc..f1f2ae6 100644 --- a/src/components/cmd-outputs/ServicesOutput.tsx +++ b/src/components/cmd-outputs/ServicesOutput.tsx @@ -76,10 +76,7 @@ function ServiceCard({ statusMsg, t, dayjs }: ServiceCardProps) {
- + {t('cmds.services.lastChecked')}{' '} {dayjs(statusMsg.timestamp).fromNow()} @@ -123,7 +120,7 @@ export default function ServicesOutput({ t }: CommandOutputProps) { return (
- {t('cmds.services.title')} + {t('cmds.services.title')} {isWaitingForHeartbeats ? (
From 943ae06b5d4c064be2d4d0506a10a46b62723d7a Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 14:23:36 -0400 Subject: [PATCH 6/8] feat: add services page with terminal emulator --- src/app/[locale]/services/page.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/app/[locale]/services/page.tsx diff --git a/src/app/[locale]/services/page.tsx b/src/app/[locale]/services/page.tsx new file mode 100644 index 0000000..411b750 --- /dev/null +++ b/src/app/[locale]/services/page.tsx @@ -0,0 +1,30 @@ +import { getTranslations } from 'next-intl/server'; +import StaticOutput from '@/components/StaticOutput'; +import TerminalEmulator from '@/components/terminal/TerminalEmulator'; +import type { RouteData } from '@/types/routing'; +import { Command } from '@/types/terminal'; +import { setPageMeta } from '@/utils/metadata-utils'; + +interface ServicesPageProps { + params: Promise<{ locale: string }>; +} + +export const generateMetadata = async (routeData: RouteData) => + await setPageMeta(routeData, 'services'); + +export default async function ServicesPage({ params }: ServicesPageProps) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: 'Terminal' }); + + return ( + <> + +
+

{t('cmds.services.title')}

+

{t('cmds.services.description')}

+
+
+ + + ); +} From d943d5bac15ff7b1051463154a80406197c4195f Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 14:23:40 -0400 Subject: [PATCH 7/8] feat: add services route and rename terminal to welcome --- src/constants/routes.ts | 3 ++- src/i18n/messages/en.json | 3 ++- src/i18n/messages/fr.json | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/constants/routes.ts b/src/constants/routes.ts index c5b52ff..ae41cf6 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -1,5 +1,6 @@ export const Routes = { - terminal: '/', + welcome: '/', whoami: '/whoami', + services: '/services', contact: '/contact', }; diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 8f8e177..6d219fd 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -13,8 +13,9 @@ "ctrlc": "CTRL+C" }, "Pages": { - "terminal": "home", + "welcome": "welcome", "whoami": "whoami", + "services": "services", "contact": "contact" }, "Status": { diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index e27fc16..bb5a756 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -13,8 +13,9 @@ "ctrlc": "CTRL+C" }, "Pages": { - "terminal": "accueil", - "whoami": "qui_suis_je", + "welcome": "bienvenue", + "whoami": "qui suis-je", + "services": "services", "contact": "contact" }, "Status": { From 5824796574cb33392167395e9ef233f515d26ffe Mon Sep 17 00:00:00 2001 From: Xavier Saliniere Date: Fri, 15 May 2026 14:38:39 -0400 Subject: [PATCH 8/8] fix: mock initializeTerminal in TerminalEmulator tests --- src/__tests__/components/TerminalEmulator.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/__tests__/components/TerminalEmulator.test.tsx b/src/__tests__/components/TerminalEmulator.test.tsx index 8f1cfd2..ea761f3 100644 --- a/src/__tests__/components/TerminalEmulator.test.tsx +++ b/src/__tests__/components/TerminalEmulator.test.tsx @@ -18,6 +18,7 @@ vi.mock('@/stores/terminal-store', () => ({ $terminalPromptRef: { set: vi.fn(), }, + initializeTerminal: vi.fn(), })); vi.mock('next-intl', () => ({ @@ -46,12 +47,14 @@ import { $terminalHistory, $terminalHistoryVisibleIdx, $terminalPromptRef, + initializeTerminal, } from '@/stores/terminal-store'; describe('TerminalEmulator', () => { const mockHistoryGet = vi.mocked($terminalHistory.get); const mockHistoryVisibleIdxGet = vi.mocked($terminalHistoryVisibleIdx.get); const mockPromptRefSet = vi.mocked($terminalPromptRef.set); + const _mockInitializeTerminal = vi.mocked(initializeTerminal); beforeEach(() => { vi.clearAllMocks();