Skip to content
Merged
3 changes: 3 additions & 0 deletions src/__tests__/components/TerminalEmulator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ vi.mock('@/stores/terminal-store', () => ({
$terminalPromptRef: {
set: vi.fn(),
},
initializeTerminal: vi.fn(),
}));

vi.mock('next-intl', () => ({
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 17 additions & 2 deletions src/app/[locale]/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <Contact />;
export default async function ContactPage({ params }: ContactPageProps) {
await params;
return (
<>
<StaticOutput>
<Contact />
</StaticOutput>
<TerminalEmulator initialCommand={Command.Contact} />
</>
);
}
34 changes: 31 additions & 3 deletions src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <TerminalEmulator />;
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 (
<>
<StaticOutput>
<div>
<p>
{t.rich('cmds.welcome.welcome', {
name: (name) => name,
})}
</p>
<p>{t('cmds.welcome.site_intro_1')}</p>
<p>
{t.rich('cmds.welcome.site_intro_2', {
cmd: () => 'help',
})}
</p>
</div>
</StaticOutput>
<TerminalEmulator initialCommand={Command.Welcome} />
</>
);
}
30 changes: 30 additions & 0 deletions src/app/[locale]/services/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<StaticOutput>
<div>
<p>{t('cmds.services.title')}</p>
<p>{t('cmds.services.description')}</p>
</div>
</StaticOutput>
<TerminalEmulator initialCommand={Command.Services} />
</>
);
}
19 changes: 17 additions & 2 deletions src/app/[locale]/whoami/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <AboutMe />;
export default async function WhoamiPage({ params }: WhoamiPageProps) {
await params;
return (
<>
<StaticOutput>
<AboutMe />
</StaticOutput>
<TerminalEmulator initialCommand={Command.Whoami} />
</>
);
}
11 changes: 11 additions & 0 deletions src/components/StaticOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
interface StaticOutputProps {
children: React.ReactNode;
}

export default function StaticOutput({ children }: StaticOutputProps) {
return (
<div aria-hidden="true" className="sr-only">
{children}
</div>
);
}
7 changes: 2 additions & 5 deletions src/components/cmd-outputs/ServicesOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,7 @@ function ServiceCard({ statusMsg, t, dayjs }: ServiceCardProps) {
</Typography>

<div className="flex items-center justify-end gap-(--lsd-spacing-smaller) mt-(--lsd-spacing-large)">
<RecordIcon
weight="duotone"
className="animate-pulse text-(--lsd-text-destructive)"
/>
<RecordIcon weight="duotone" className="animate-pulse" />
<Typography variant="body3">
{t('cmds.services.lastChecked')}{' '}
{dayjs(statusMsg.timestamp).fromNow()}
Expand Down Expand Up @@ -123,7 +120,7 @@ export default function ServicesOutput({ t }: CommandOutputProps) {

return (
<div className="py-(--lsd-spacing-small)">
<Typography variant="body1">{t('cmds.services.title')}</Typography>
<Typography variant="body2">{t('cmds.services.title')}</Typography>

{isWaitingForHeartbeats ? (
<div className="flex items-center gap-(--lsd-spacing-small) mt-(--lsd-spacing-small)">
Expand Down
17 changes: 16 additions & 1 deletion src/components/terminal/TerminalEmulator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
'use client';

import { useStore } from '@nanostores/react';
import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
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);

Expand All @@ -33,6 +42,12 @@ export default function TerminalEmulator() {
}
}, [hasWindow]);

useEffect(() => {
if (hasWindow) {
initializeTerminal(initialCommand);
}
}, [hasWindow, initialCommand]);

return (
hasWindow && (
<div className="size-full overflow-y-auto text-(length:--lsd-body2-fontSize) sm:text-(length:--lsd-body1-fontSize)">
Expand Down
3 changes: 2 additions & 1 deletion src/constants/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const Routes = {
terminal: '/',
welcome: '/',
whoami: '/whoami',
services: '/services',
contact: '/contact',
};
5 changes: 3 additions & 2 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"ctrlc": "CTRL+C"
},
"Pages": {
"terminal": "home",
"welcome": "welcome",
"whoami": "whoami",
"services": "services",
"contact": "contact"
},
"Status": {
Expand Down Expand Up @@ -66,7 +67,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...",
Expand Down
7 changes: 4 additions & 3 deletions src/i18n/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -66,7 +67,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...",
Expand Down
20 changes: 9 additions & 11 deletions src/stores/terminal-store.ts
Original file line number Diff line number Diff line change
@@ -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('');
Expand All @@ -17,15 +17,6 @@ export const $terminalPromptRef =
atom<RefObject<TerminalPromptRef | null> | 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;
Expand Down Expand Up @@ -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();
Expand Down
Loading