diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md deleted file mode 100644 index 62875cb..0000000 --- a/.github/instructions/ai.instructions.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -applyTo: "**" ---- - -# AI Documentation Guidelines - -## ⚠️ STOP - READ THIS FIRST - -**BEFORE doing ANYTHING else, you MUST follow this workflow:** - -### Required Workflow - -1. **First, check for `.ai` directory documentation** - ```bash - # Search .ai docs for relevant keywords - grep -r "relevant_keywords" .ai/ - ``` - -2. **Read applicable documentation in this order:** - - `.ai/README.md` - Project overview and quick start - - `.ai/ARCHITECTURE.md` - Key components and architecture - - `.ai/DEVELOPMENT.md` - Setup and coding conventions - - `.ai/COMMANDS.md` - Command system documentation (if working with commands) - -3. **Acknowledge understanding by stating:** - - "After reading .ai/[FILE].md, I understand that..." - - Reference specific patterns or conventions you'll follow - -4. **THEN proceed with your task** - -## Purpose - -The `.ai` directory contains comprehensive documentation to help AI agents quickly understand and continue development on the project. It serves as a knowledge base for the project's architecture, features, and development process. - -## Usage Guidelines - -1. **ALWAYS consult the `.ai` documentation when starting work on the project** -2. Use the documentation to understand the project structure and conventions -3. Reference specific patterns from the docs in your implementation -4. When in doubt, search the `.ai` files for guidance - -## Maintenance - -1. Update the documentation when making significant changes to the project -2. Keep the documentation accurate and up-to-date -3. Add new sections when introducing major features or architectural changes -4. Remove outdated information promptly - -## Decision Making - -1. Use the documentation as a reference when making architectural decisions -2. Follow established patterns and conventions documented in `.ai` -3. Consider the impact of changes on documented components and features -4. Update documentation to reflect any approved changes diff --git a/README.md b/README.md index 0ac0ea2..18940ad 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ Still a work in progress, but it already lives on the InterPlanetary FileSystem: - [x] responsive design - [x] internationalization - [x] automatic deployment on IPFS -- [] content is still being written -- [] pages are still being added & refined +- [ ] UI still needs to be improved +- [ ] content is still being written +- [ ] pages are still being added & refined ## commands diff --git a/package.json b/package.json index 81eb355..eb6acf6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "postinstall": "lefthook install" }, "dependencies": { - "@acid-info/lsd-react": "0.2.0-beta.4", + "@nipsysdev/lsd-react": "0.2.0-beta.4-dev1", "next": "^15.5.2", "next-intl": "^4.3.5", "postcss": "^8.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9aab01..13a2b58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@acid-info/lsd-react': - specifier: 0.2.0-beta.4 - version: 0.2.0-beta.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@nipsysdev/lsd-react': + specifier: 0.2.0-beta.4-dev1 + version: 0.2.0-beta.4-dev1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next: specifier: ^15.5.2 version: 15.5.2(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -81,12 +81,6 @@ importers: packages: - '@acid-info/lsd-react@0.2.0-beta.4': - resolution: {integrity: sha512-SkpJmJhrDuGmAgwL04o8mjGPJR0jAo04g/wNAddwhoZ4RhQqvEIBPlK98slyn8XmiWJhddT9rf54YCGIuHc00A==} - peerDependencies: - react: 17.x || 18.x || 19.x - react-dom: 17.x || 18.x || 19.x - '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -663,6 +657,12 @@ packages: cpu: [x64] os: [win32] + '@nipsysdev/lsd-react@0.2.0-beta.4-dev1': + resolution: {integrity: sha512-bRnxuyKvpS8f9uXRL7W6oukzGdqk907b2FNFw+c3usyGUssaq4lfoKG0Km3+RBhCeeNhefJiHMc4f1MhONA5TA==} + peerDependencies: + react: 17.x || 18.x || 19.x + react-dom: 17.x || 18.x || 19.x + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2066,16 +2066,6 @@ packages: snapshots: - '@acid-info/lsd-react@0.2.0-beta.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': - dependencies: - '@datepicker-react/hooks': 2.8.4(react@19.1.1) - clsx: 1.2.1 - lodash: 4.17.21 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-hot-toast: 2.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - react-use: 17.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@adobe/css-tools@4.4.4': {} '@alloc/quick-lru@5.2.0': {} @@ -2537,6 +2527,16 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.2': optional: true + '@nipsysdev/lsd-react@0.2.0-beta.4-dev1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@datepicker-react/hooks': 2.8.4(react@19.1.1) + clsx: 1.2.1 + lodash: 4.17.21 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-hot-toast: 2.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react-use: 17.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/src/app/[locale]/contribs/page.tsx b/src/app/[locale]/contribs/page.tsx new file mode 100644 index 0000000..47def9a --- /dev/null +++ b/src/app/[locale]/contribs/page.tsx @@ -0,0 +1,10 @@ +import Contribs from '@/components/contribs/Contribs'; +import type { RouteData } from '@/types/routing'; +import { setPageMeta } from '@/utils/metadata-utils'; + +export const generateMetadata = async (routeData: RouteData) => + await setPageMeta(routeData, 'contribs'); + +export default function ContribsPage() { + return ; +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index f745929..f728de5 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,8 +1,7 @@ import { notFound } from 'next/navigation'; import { hasLocale, NextIntlClientProvider } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; -import Header from '@/components/layout/Header'; -import TerminalWindow from '@/components/layout/TermWindow'; +import MainWrapper from '@/components/layout/MainWrapper'; import { routing } from '@/i18n/intl'; import type { RouteData } from '@/types/routing'; import { setPageMeta } from '@/utils/metadata-utils'; @@ -29,13 +28,7 @@ export default async function Layout({ return ( -
-
- -
- {children} -
-
+ {children}
); } diff --git a/src/app/[locale]/mission/page.tsx b/src/app/[locale]/mission/page.tsx deleted file mode 100644 index b777840..0000000 --- a/src/app/[locale]/mission/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Web3Mission from '@/components/web3-mission/Web3Mission'; -import type { RouteData } from '@/types/routing'; -import { setPageMeta } from '@/utils/metadata-utils'; - -export const generateMetadata = async (routeData: RouteData) => - await setPageMeta(routeData, 'mission'); - -export default function MissionPage() { - return ; -} diff --git a/src/app/[locale]/web2work/page.tsx b/src/app/[locale]/web2work/page.tsx new file mode 100644 index 0000000..b9549ee --- /dev/null +++ b/src/app/[locale]/web2work/page.tsx @@ -0,0 +1,10 @@ +import Web2work from '@/components/web2work/Web2work'; +import type { RouteData } from '@/types/routing'; +import { setPageMeta } from '@/utils/metadata-utils'; + +export const generateMetadata = async (routeData: RouteData) => + await setPageMeta(routeData, 'web2work'); + +export default function Web3workPage() { + return ; +} diff --git a/src/app/[locale]/web3work/page.tsx b/src/app/[locale]/web3work/page.tsx new file mode 100644 index 0000000..76ef013 --- /dev/null +++ b/src/app/[locale]/web3work/page.tsx @@ -0,0 +1,10 @@ +import Web3work from '@/components/web3work/Web3work'; +import type { RouteData } from '@/types/routing'; +import { setPageMeta } from '@/utils/metadata-utils'; + +export const generateMetadata = async (routeData: RouteData) => + await setPageMeta(routeData, 'web3work'); + +export default function Web3workPage() { + return ; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2af1061..67f83b6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,9 @@ -import { LsdThemeStyles } from '@acid-info/lsd-react/theme'; import { getTranslations } from 'next-intl/server'; import { AppStateProvider } from '@/contexts/AppContext'; import 'tailwindcss/index.css'; -import '@acid-info/lsd-react/css'; - -// Initialize chunk retry manager for IPFS deployment +import '@nipsysdev/lsd-react/css'; import '@/utils/chunk-retry'; +import { LsdThemeStyles } from '@nipsysdev/lsd-react/theme'; export async function generateMetadata() { const tMeta = await getTranslations({ locale: 'en', namespace: 'Metadata' }); @@ -31,7 +29,7 @@ export default function RootLayout({ - + {children} diff --git a/src/components/__tests__/LoadSequence.test.tsx b/src/components/__tests__/LoadSequence.test.tsx index e51387a..57aa4ba 100644 --- a/src/components/__tests__/LoadSequence.test.tsx +++ b/src/components/__tests__/LoadSequence.test.tsx @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import LoadSequence from '../LoadSequence'; // Mock Typography component -vi.mock('@acid-info/lsd-react/client/Typography', () => ({ +vi.mock('@nipsysdev/lsd-react/client/Typography', () => ({ Typography: ({ children, ...props }: { children: React.ReactNode }) => (
{children}
), diff --git a/src/components/cmd-outputs/ContribsOutput.tsx b/src/components/cmd-outputs/ContribsOutput.tsx new file mode 100644 index 0000000..10f86a6 --- /dev/null +++ b/src/components/cmd-outputs/ContribsOutput.tsx @@ -0,0 +1,9 @@ +import { Component } from 'react'; +import type { CommandOutputProps } from '@/types/terminal'; +import Contribs from '../contribs/Contribs'; + +export default class ContribsOutput extends Component { + render() { + return ; + } +} diff --git a/src/components/cmd-outputs/HelpOutput.tsx b/src/components/cmd-outputs/HelpOutput.tsx index a22b5e2..7b0bddb 100644 --- a/src/components/cmd-outputs/HelpOutput.tsx +++ b/src/components/cmd-outputs/HelpOutput.tsx @@ -1,4 +1,4 @@ -import { Typography } from '@acid-info/lsd-react/client/Typography'; +import { Typography } from '@nipsysdev/lsd-react/client/Typography'; import { Component } from 'react'; import { Commands } from '@/constants/commands'; import type { CommandOutputProps } from '@/types/terminal'; diff --git a/src/components/cmd-outputs/Web2workOutput.tsx b/src/components/cmd-outputs/Web2workOutput.tsx new file mode 100644 index 0000000..489e114 --- /dev/null +++ b/src/components/cmd-outputs/Web2workOutput.tsx @@ -0,0 +1,9 @@ +import { Component } from 'react'; +import type { CommandOutputProps } from '@/types/terminal'; +import Web2work from '../web2work/Web2work'; + +export default class Web2workOutput extends Component { + render() { + return ; + } +} diff --git a/src/components/cmd-outputs/Web3MissionOutput.tsx b/src/components/cmd-outputs/Web3MissionOutput.tsx deleted file mode 100644 index 7be0101..0000000 --- a/src/components/cmd-outputs/Web3MissionOutput.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from 'react'; -import type { CommandOutputProps } from '@/types/terminal'; -import Web3Mission from '../web3-mission/Web3Mission'; - -export default class Web3MissionOutput extends Component { - render() { - return ; - } -} diff --git a/src/components/cmd-outputs/Web3workOutput.tsx b/src/components/cmd-outputs/Web3workOutput.tsx new file mode 100644 index 0000000..2a881f3 --- /dev/null +++ b/src/components/cmd-outputs/Web3workOutput.tsx @@ -0,0 +1,9 @@ +import { Component } from 'react'; +import type { CommandOutputProps } from '@/types/terminal'; +import Web3work from '../web3work/Web3work'; + +export default class Web3workOutput extends Component { + render() { + return ; + } +} diff --git a/src/components/cmd-outputs/IntroOutput.tsx b/src/components/cmd-outputs/WelcomeOutput.tsx similarity index 73% rename from src/components/cmd-outputs/IntroOutput.tsx rename to src/components/cmd-outputs/WelcomeOutput.tsx index d8cff1d..cdfb0ea 100644 --- a/src/components/cmd-outputs/IntroOutput.tsx +++ b/src/components/cmd-outputs/WelcomeOutput.tsx @@ -3,7 +3,7 @@ import asciiArt from '@/assets/ascii-art'; import { Command, type CommandOutputProps } from '@/types/terminal'; import CmdLink from '../terminal/CmdLink'; -export default class IntroOutput extends Component { +export default class WelcomeOutput extends Component { render() { return (
@@ -12,14 +12,14 @@ export default class IntroOutput extends Component {

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

-

{this.props.t('cmds.intro.site_intro_1')}

+

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

- {this.props.t.rich('cmds.intro.site_intro_2', { + {this.props.t.rich('cmds.welcome.site_intro_2', { cmd: () => , })}

diff --git a/src/components/cmd-outputs/__tests__/ContribsOutput.test.tsx b/src/components/cmd-outputs/__tests__/ContribsOutput.test.tsx new file mode 100644 index 0000000..50f843b --- /dev/null +++ b/src/components/cmd-outputs/__tests__/ContribsOutput.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { Translator } from '@/i18n/intl'; +import { Command, type CommandEntry } from '@/types/terminal'; +import ContribsOutput from '../ContribsOutput'; + +vi.mock('../contribs/Contribs', () => ({ + default: () =>
contribs / todo
, +})); + +describe('ContribsOutput', () => { + const mockT = vi.fn((key: string) => key) as unknown as Translator; + const mockEntry: CommandEntry = { + timestamp: Date.now(), + cmdName: Command.Contribs, + }; + + it('renders the Contribs component', () => { + render(); + expect(screen.getByText('contribs / todo')).toBeInTheDocument(); + }); +}); diff --git a/src/components/cmd-outputs/__tests__/HelpOutput.test.tsx b/src/components/cmd-outputs/__tests__/HelpOutput.test.tsx index 78a80ec..1371f9a 100644 --- a/src/components/cmd-outputs/__tests__/HelpOutput.test.tsx +++ b/src/components/cmd-outputs/__tests__/HelpOutput.test.tsx @@ -49,7 +49,7 @@ vi.mock('@/contexts/TerminalContext', () => ({ })); // Mock LSD React Typography component -vi.mock('@acid-info/lsd-react/client/Typography', () => ({ +vi.mock('@nipsysdev/lsd-react/client/Typography', () => ({ Typography: ({ children, variant, diff --git a/src/components/cmd-outputs/__tests__/Web2workOutput.test.tsx b/src/components/cmd-outputs/__tests__/Web2workOutput.test.tsx new file mode 100644 index 0000000..e000be4 --- /dev/null +++ b/src/components/cmd-outputs/__tests__/Web2workOutput.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { Translator } from '@/i18n/intl'; +import { Command, type CommandEntry } from '@/types/terminal'; +import Web2workOutput from '../Web2workOutput'; + +vi.mock('../web2work/Web2work', () => ({ + default: () =>
web2work / todo
, +})); + +describe('Web2workOutput', () => { + const mockT = vi.fn((key: string) => key) as unknown as Translator; + const mockEntry: CommandEntry = { + timestamp: Date.now(), + cmdName: Command.Web2work, + }; + + it('renders the Web2work component', () => { + render(); + expect(screen.getByText('web2work / todo')).toBeInTheDocument(); + }); +}); diff --git a/src/components/cmd-outputs/__tests__/Web3MissionOutput.test.tsx b/src/components/cmd-outputs/__tests__/Web3MissionOutput.test.tsx deleted file mode 100644 index e88317c..0000000 --- a/src/components/cmd-outputs/__tests__/Web3MissionOutput.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import type { Translator } from '@/i18n/intl'; -import { Command, type CommandEntry } from '@/types/terminal'; -import Web3MissionOutput from '../Web3MissionOutput'; - -// Mock the Web3Mission component - it just renders simple text -vi.mock('../web3-mission/Web3Mission', () => ({ - default: () =>
web3 mission / todo
, -})); - -describe('Web3MissionOutput', () => { - const mockT = vi.fn((key: string) => key) as unknown as Translator; - const mockEntry: CommandEntry = { - timestamp: Date.now(), - cmdName: Command.Web3Mission, - }; - - it('renders the Web3Mission component', () => { - render(); - expect(screen.getByText('web3 mission / todo')).toBeInTheDocument(); - }); -}); diff --git a/src/components/cmd-outputs/__tests__/Web3workOutput.test.tsx b/src/components/cmd-outputs/__tests__/Web3workOutput.test.tsx new file mode 100644 index 0000000..6cdaee8 --- /dev/null +++ b/src/components/cmd-outputs/__tests__/Web3workOutput.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { Translator } from '@/i18n/intl'; +import { Command, type CommandEntry } from '@/types/terminal'; +import Web3workOutput from '../Web3workOutput'; + +vi.mock('../web3work/Web3work', () => ({ + default: () =>
web3work / todo
, +})); + +describe('Web3workOutput', () => { + const mockT = vi.fn((key: string) => key) as unknown as Translator; + const mockEntry: CommandEntry = { + timestamp: Date.now(), + cmdName: Command.Web3work, + }; + + it('renders the Web3work component', () => { + render(); + expect(screen.getByText('web3work / todo')).toBeInTheDocument(); + }); +}); diff --git a/src/components/cmd-outputs/__tests__/IntroOutput.test.tsx b/src/components/cmd-outputs/__tests__/WelcomeOutput.test.tsx similarity index 76% rename from src/components/cmd-outputs/__tests__/IntroOutput.test.tsx rename to src/components/cmd-outputs/__tests__/WelcomeOutput.test.tsx index d840d66..f449450 100644 --- a/src/components/cmd-outputs/__tests__/IntroOutput.test.tsx +++ b/src/components/cmd-outputs/__tests__/WelcomeOutput.test.tsx @@ -3,7 +3,7 @@ import type { ReactElement } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Translator } from '@/i18n/intl'; import { Command, type CommandEntry } from '@/types/terminal'; -import IntroOutput from '../IntroOutput'; +import WelcomeOutput from '../WelcomeOutput'; // Mock the CmdLink component vi.mock('../terminal/CmdLink', () => ({ @@ -12,7 +12,7 @@ vi.mock('../terminal/CmdLink', () => ({ ), })); -describe('IntroOutput', () => { +describe('WelcomeOutput', () => { const mockT = vi.fn((key: string) => key) as unknown as Translator; const mockTRich = vi.fn((key: string, values?: Record) => { if (values && typeof values.name === 'function') { @@ -27,7 +27,7 @@ describe('IntroOutput', () => { const mockTWithRich = Object.assign(mockT, { rich: mockTRich }); const mockEntry: CommandEntry = { timestamp: Date.now(), - cmdName: Command.Intro, + cmdName: Command.Welcome, }; beforeEach(() => { @@ -35,25 +35,25 @@ describe('IntroOutput', () => { }); it('renders welcome message', () => { - render(); + render(); expect(mockTRich).toHaveBeenCalledWith( - 'cmds.intro.welcome', + 'cmds.welcome.welcome', expect.any(Object), ); }); - it('renders site introduction text', () => { - render(); - expect(mockT).toHaveBeenCalledWith('cmds.intro.site_intro_1'); + it('renders site welcome text', () => { + render(); + expect(mockT).toHaveBeenCalledWith('cmds.welcome.site_intro_1'); expect(mockTRich).toHaveBeenCalledWith( - 'cmds.intro.site_intro_2', + 'cmds.welcome.site_intro_2', expect.any(Object), ); }); it('renders ascii art', () => { const { container } = render( - , + , ); const asciiDiv = container.querySelector('.whitespace-break-spaces'); diff --git a/src/components/contribs/Contribs.tsx b/src/components/contribs/Contribs.tsx new file mode 100644 index 0000000..b0cef0c --- /dev/null +++ b/src/components/contribs/Contribs.tsx @@ -0,0 +1,3 @@ +export default function Contribs() { + return
contribs / todo
; +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index c8ba61d..51ec921 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,19 +1,25 @@ 'use client'; -import { Button } from '@acid-info/lsd-react/client/Button'; -import { ButtonGroup } from '@acid-info/lsd-react/client/ButtonGroup'; -import { useLocale } from 'next-intl'; +import { Button } from '@nipsysdev/lsd-react/client/Button'; +import { ButtonGroup } from '@nipsysdev/lsd-react/client/ButtonGroup'; +import { useLocale, useTranslations } from 'next-intl'; import { PiGithubLogoFill } from 'react-icons/pi'; import { LangLabels } from '@/constants/lang'; +import { useAppContext } from '@/contexts/AppContext'; import { Link, usePathname } from '@/i18n/intl'; import styles from '@/styles/components.module.css'; import { cx } from '@/utils/helpers'; export default function Header() { + const t = useTranslations('Core'); + const { setIsMenuDisplayed } = useAppContext(); const pathname = usePathname(); const locale = useLocale(); return ( -
+
+
+ +
{Object.entries(LangLabels).map(([lang, label]) => ( + ))} + + ); + + return ( +
+
+ {btnList} +
+ + setIsMenuDisplayed(false)} + className={styles.modal} + > + +
{btnList}
+
+
+
+ ); +} diff --git a/src/components/layout/TermWindow.tsx b/src/components/layout/TermWindow.tsx deleted file mode 100644 index 1cd1d26..0000000 --- a/src/components/layout/TermWindow.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; -import { TabItem } from '@acid-info/lsd-react/client/TabItem'; -import { Tabs } from '@acid-info/lsd-react/client/Tabs'; -import { useTranslations } from 'next-intl'; -import { Routes } from '@/constants/routes'; -import { Link, usePathname } from '@/i18n/intl'; -import styles from '@/styles/components.module.css'; - -export default function TerminalWindow({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const t = useTranslations('Pages'); - - const pathname = usePathname(); - const notfound = '404'; - const activeRouteName = - Object.entries(Routes) - .filter(([, routePath]) => routePath === pathname) - .map(([routeName]) => routeName) - .find(Boolean) ?? notfound; - - return ( -
- - {Object.entries(Routes).map(([routeName, routePath]) => ( - - {t(routeName)} - - ))} - - {children} -
- ); -} diff --git a/src/components/layout/__tests__/Header.test.tsx b/src/components/layout/__tests__/Header.test.tsx index 1161d14..f7c55eb 100644 --- a/src/components/layout/__tests__/Header.test.tsx +++ b/src/components/layout/__tests__/Header.test.tsx @@ -1,11 +1,18 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { LangLabels } from '@/constants/lang'; -import Header from '../Header'; + +// Mock AppContext +const mockSetIsMenuDisplayed = vi.fn(); +vi.mock('@/contexts/AppContext', () => ({ + useAppContext: () => ({ + setIsMenuDisplayed: mockSetIsMenuDisplayed, + }), +})); // Mock next-intl vi.mock('next-intl', () => ({ useLocale: () => 'en', + useTranslations: () => (key: string) => key, })); // Mock the i18n navigation @@ -27,21 +34,26 @@ vi.mock('@/i18n/intl', () => ({ })); // Mock LSD React components -vi.mock('@acid-info/lsd-react/client/Button', () => ({ +vi.mock('@nipsysdev/lsd-react/client/Button', () => ({ Button: ({ children, + onClick, variant, size, className, }: { children: React.ReactNode; - variant: string; - size: string; + onClick?: () => void; + variant?: string; + size?: string; className?: string; }) => ( + ), +})); + +vi.mock('@nipsysdev/lsd-react/client/Modal', () => ({ + Modal: ({ + isOpen, + onClose, + children, + className, + }: { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; + className?: string; + }) => + isOpen ? ( +
+ + {children} +
+ ) : null, +})); + +vi.mock('@nipsysdev/lsd-react/client/ModalBody', () => ({ + ModalBody: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +describe('Sidenav', () => { + beforeEach(() => { + // Reset mocks and state before each test + vi.clearAllMocks(); + mockIsMenuDisplayed = false; + mockSetIsMenuDisplayed = vi.fn(); + mockUsePathname = () => '/'; + + mockLink = vi.fn( + ({ + children, + href, + onClick, + }: { + children: React.ReactNode; + href: string; + onClick?: () => void; + }) => ( + + {children} + + ), + ); + }); + + it('renders desktop sidebar with navigation links', () => { + render(); + + // The sidebar itself is a div, not a nav role, so we check for a child link + const firstLink = screen.getByRole('link', { + name: Object.keys(Routes)[0], + }); + const sidebar = firstLink.closest('div.flex-col'); + expect(sidebar).toBeInTheDocument(); + expect(sidebar).toHaveClass('hidden'); // It's hidden on small screens by default + + Object.entries(Routes).forEach(([routeName, routePath]) => { + const link = screen.getByRole('link', { name: routeName }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', routePath); + }); + }); + + it('does not render modal when isMenuDisplayed is false', () => { + render(); + expect(screen.queryByTestId('modal')).not.toBeInTheDocument(); + }); + + it('renders modal when isMenuDisplayed is true', () => { + mockIsMenuDisplayed = true; + render(); + + const modal = screen.getByTestId('modal'); + expect(modal).toBeInTheDocument(); + expect(screen.getByTestId('modal-body')).toBeInTheDocument(); + + // Scope the link checks to within the modal + Object.entries(Routes).forEach(([routeName, routePath]) => { + const link = within(modal).getByRole('link', { name: routeName }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', routePath); + }); + }); + + it('calls setIsMenuDisplayed(false) when modal close button is clicked', () => { + mockIsMenuDisplayed = true; + render(); + + const closeButton = screen.getByTestId('modal-close'); + fireEvent.click(closeButton); + + expect(mockSetIsMenuDisplayed).toHaveBeenCalledTimes(1); + expect(mockSetIsMenuDisplayed).toHaveBeenCalledWith(false); + }); + + it('calls setIsMenuDisplayed(false) when a link inside the modal is clicked', () => { + mockIsMenuDisplayed = true; + render(); + + const modal = screen.getByTestId('modal'); + const firstRouteName = Object.keys(Routes)[0]; + // Scope the link query to within the modal + const firstLink = within(modal).getByRole('link', { name: firstRouteName }); + fireEvent.click(firstLink); + + expect(mockSetIsMenuDisplayed).toHaveBeenCalledTimes(1); + expect(mockSetIsMenuDisplayed).toHaveBeenCalledWith(false); + }); + + it('highlights the active route in the desktop sidebar', () => { + const activeRoute = 'whoami'; // Example active route + const activePath = Routes[activeRoute as keyof typeof Routes]; + mockUsePathname = () => activePath; + + render(); + + const activeButton = screen.getByTestId('button-filled'); + expect(activeButton).toBeInTheDocument(); + // Check that the link inside the active button has the correct name + expect(activeButton).toHaveTextContent(activeRoute); + + // Check that other buttons are 'outlined' + Object.keys(Routes).forEach((routeName) => { + if (routeName !== activeRoute) { + // We need to be more specific to get the correct button + const link = screen.getByRole('link', { name: routeName }); + const button = link.closest('button'); + expect(button).toHaveAttribute('data-testid', 'button-outlined'); + } + }); + }); + + it('highlights the active route in the modal', () => { + const activeRoute = 'web3work'; // Example active route + const activePath = Routes[activeRoute as keyof typeof Routes]; + mockUsePathname = () => activePath; + mockIsMenuDisplayed = true; + + render(); + + const modal = screen.getByTestId('modal'); + // Scope the button query to within the modal + const activeButton = within(modal).getByTestId('button-filled'); + expect(activeButton).toBeInTheDocument(); + expect(activeButton).toHaveTextContent(activeRoute); + }); +}); diff --git a/src/components/layout/__tests__/TermWindow.test.tsx b/src/components/layout/__tests__/TermWindow.test.tsx deleted file mode 100644 index 9dda2bf..0000000 --- a/src/components/layout/__tests__/TermWindow.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { Routes } from '@/constants/routes'; -import TerminalWindow from '../TermWindow'; - -// Mock next-intl -vi.mock('next-intl', () => ({ - useTranslations: () => (key: string) => key, -})); - -// Mock the i18n navigation -vi.mock('@/i18n/intl', () => ({ - Link: ({ children, href }: { children: React.ReactNode; href: string }) => ( - {children} - ), - usePathname: () => '/mission', -})); - -// Mock LSD React components -vi.mock('@acid-info/lsd-react/client/Tabs', () => ({ - Tabs: ({ - children, - className, - activeTab, - fullWidth, - }: { - children: React.ReactNode; - className?: string; - activeTab: string; - fullWidth?: boolean; - }) => ( -
- {children} -
- ), -})); - -vi.mock('@acid-info/lsd-react/client/TabItem', () => ({ - TabItem: ({ - children, - name, - className, - }: { - children: React.ReactNode; - name: string; - className?: string; - }) => ( -
- {children} -
- ), -})); - -describe('TerminalWindow', () => { - const mockChildren =
Test content
; - - it('sets correct active tab based on pathname', () => { - render({mockChildren}); - - const tabs = screen.getByTestId('tabs'); - expect(tabs).toHaveAttribute('data-active-tab', 'mission'); - }); - - it('renders tab items for all routes', () => { - render({mockChildren}); - - Object.entries(Routes).forEach(([routeName, routePath]) => { - const tabItem = screen.getByTestId(`tab-item-${routeName}`); - expect(tabItem).toBeInTheDocument(); - expect(tabItem).toHaveAttribute('data-name', routeName); - - // Check the link inside the tab - const link = screen.getByRole('link', { name: routeName }); - expect(link).toHaveAttribute('href', routePath); - }); - }); - - it('renders children content', () => { - render({mockChildren}); - expect(screen.getByTestId('mock-children')).toBeInTheDocument(); - }); -}); diff --git a/src/components/terminal/CmdLink.tsx b/src/components/terminal/CmdLink.tsx index f8bb10e..99a53de 100644 --- a/src/components/terminal/CmdLink.tsx +++ b/src/components/terminal/CmdLink.tsx @@ -1,4 +1,4 @@ -import { Button } from '@acid-info/lsd-react/client/Button'; +import { Button } from '@nipsysdev/lsd-react/client/Button'; import type { JSX } from 'react'; import { useTerminalContext } from '@/contexts/TerminalContext'; import type { CommandArgument, CommandInfo } from '@/types/terminal'; diff --git a/src/components/terminal/TerminalEmulator.tsx b/src/components/terminal/TerminalEmulator.tsx index d8d0d8c..23c8d8a 100644 --- a/src/components/terminal/TerminalEmulator.tsx +++ b/src/components/terminal/TerminalEmulator.tsx @@ -10,12 +10,12 @@ import TerminalPrompt from './TerminalPrompt'; export default function TerminalEmulator() { const { - hasIntroduced, + hasWelcomed, history, input, simulatedCmd, submission, - setHasIntroduced, + setHasWelcomed, setHasRefreshed, setHistory, setInput, @@ -38,11 +38,11 @@ export default function TerminalEmulator() { useEffect(() => { mainPrompt.current?.focus(); - if (!isPrerender && !hasIntroduced && mainPrompt.current) { - setHasIntroduced(true); - mainPrompt.current?.simulate(Command.Intro); + if (!isPrerender && !hasWelcomed && mainPrompt.current) { + setHasWelcomed(true); + mainPrompt.current?.simulate(Command.Welcome); } - }, [hasIntroduced, isPrerender, mainPrompt, setHasIntroduced]); + }, [hasWelcomed, isPrerender, mainPrompt, setHasWelcomed]); useEffect(() => { if (!submission) return; @@ -79,7 +79,7 @@ export default function TerminalEmulator() { return ( hasWindow && ( -
+
{/** biome-ignore lint/a11y/useSemanticElements: terminal container needs to be clickable and listen to inputs while still displaying as a div */}
({ })); // Mock the LSD Button component -vi.mock('@acid-info/lsd-react/client/Button', () => ({ +vi.mock('@nipsysdev/lsd-react/client/Button', () => ({ Button: ({ children, onClick, ...props }: React.ComponentProps<'button'>) => ( + +