diff --git a/.ai/DEVELOPMENT.md b/.ai/DEVELOPMENT.md index 5435631..369c113 100644 --- a/.ai/DEVELOPMENT.md +++ b/.ai/DEVELOPMENT.md @@ -20,6 +20,17 @@ - Test framework: Vitest with React Testing Library - Coverage reporting: V8 coverage provider +### Testing Requirements + +- **New functionality must include tests** - Never implement new features without corresponding tests +- **Update existing tests** when modifying functionality +- **Test all edge cases** - especially for utility functions and complex logic +- **Component tests** should cover props, state changes, and user interactions +- **Utility tests** should cover all branches and error conditions +- **Integration tests** for command system and terminal interactions +- **Mock external dependencies** appropriately in tests +- **Test file organization** - keep tests in `__tests__/` directories alongside components + ## Quality Assurance - Linting: `pnpm lint` (Biome) diff --git a/README.md b/README.md index b47c1a4..1391bc6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# nipsys.dev +# nipsys.eth site ``` $ whoami @@ -13,7 +13,7 @@ built this when i got tired of the usual portfolio sites. figured if i'm gonna s licensed under GPL-3.0 because that's how it should be. -**status**: work in progress. not even hosted yet, but getting there. +**status**: work in progress. live on IPFS via IPNS `k2k4r8ng8uzrtqb5ham8kao889m8qezu96z4w3lpinyqghum43veb6n3` ## why a terminal? @@ -28,11 +28,10 @@ licensed under GPL-3.0 because that's how it should be. - βœ… commands and navigation - βœ… responsive design - βœ… internationalization +- βœ… deployed on IPFS - 🚧 content still being written -- 🚧 ipfs deployment setup +- 🚧 automatic ipfs deployment setup - 🚧 figuring out codex hosting -- 🚧 web3 integration features -- ❌ not live yet (soonβ„’) ## commands diff --git a/next.config.ts b/next.config.ts index 268dfc1..98826f9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,8 @@ import createNextIntlPlugin from 'next-intl/plugin'; const nextConfig: NextConfig = { output: 'export', + trailingSlash: true, + images: { unoptimized: true }, }; const withNextIntl = createNextIntlPlugin(); diff --git a/src/components/terminal/TerminalPrompt.tsx b/src/components/terminal/TerminalPrompt.tsx index fb30ee9..1991f53 100644 --- a/src/components/terminal/TerminalPrompt.tsx +++ b/src/components/terminal/TerminalPrompt.tsx @@ -32,15 +32,55 @@ interface State { } export default class TerminalPrompt extends Component { + // Constants for IPFS host display formatting + private static readonly MAX_HOST_LENGTH = 25; + private static readonly CID_MIN_LENGTH = 16; + private static readonly CID_TRUNCATION_LENGTH = 8; + state: State = { inputText: '', inputRef: createRef(), autocompleteRef: createRef(), historyIdx: -1, autocomplete: null, - host: window.location.host.split(':')[0], + host: this.getDisplayHost(), }; + private getDisplayHost(): string { + const fullHost = window.location.host.split(':')[0]; + + // Check if it's an IPFS host (contains ipns or ipfs and is long) + if (fullHost.includes('ipns') || fullHost.includes('ipfs')) { + if (fullHost.length > TerminalPrompt.MAX_HOST_LENGTH) { + // For IPFS, show first CID_TRUNCATION_LENGTH chars + ... + last CID_TRUNCATION_LENGTH chars of the CID + domain + const parts = fullHost.split('.'); + const cidPart = parts[0]; // The long hash part + + // Validate that we have domain parts before constructing domainPart + let domainPart = ''; + if (parts.length > 1) { + domainPart = parts.slice(1).join('.'); // Everything after the CID + } else { + // Fallback to fullHost if no '.' is present (edge case) + return fullHost; + } + + if (cidPart.length > TerminalPrompt.CID_MIN_LENGTH) { + const prefix = cidPart.substring( + 0, + TerminalPrompt.CID_TRUNCATION_LENGTH, + ); + const suffix = cidPart.substring( + cidPart.length - TerminalPrompt.CID_TRUNCATION_LENGTH, + ); + return `${prefix}...${suffix}.${domainPart}`; + } + } + } + + return fullHost; + } + componentDidMount(): void { if (this.props.entry) { this.setInput(getTerminalEntryInput(this.props.entry)); diff --git a/src/components/terminal/__tests__/TerminalPrompt.test.tsx b/src/components/terminal/__tests__/TerminalPrompt.test.tsx index 52b01c5..fb85ad2 100644 --- a/src/components/terminal/__tests__/TerminalPrompt.test.tsx +++ b/src/components/terminal/__tests__/TerminalPrompt.test.tsx @@ -148,7 +148,14 @@ describe('TerminalPrompt', () => { it('renders Terminal prompt span', () => { render(); - const promptSpan = screen.getByText('translated_visitor@localhost:~$'); + // Check for the terminal prompt span with a more flexible matcher + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('translated_visitor') && + content.includes(':~$') + ); + }); expect(promptSpan).toBeInTheDocument(); }); @@ -836,4 +843,94 @@ describe('TerminalPrompt', () => { expect(mockSetLastKeyDown).toHaveBeenCalledTimes(2); }); }); + + describe('getDisplayHost', () => { + it('returns full host for regular domains', () => { + Object.defineProperty(window, 'location', { + value: { host: 'example.com' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@example.com:~$'), + ).toBeInTheDocument(); + }); + + it('returns full host for short IPFS hosts', () => { + Object.defineProperty(window, 'location', { + value: { host: 'short.ipfs.dweb.link' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@short.ipfs.dweb.link:~$'), + ).toBeInTheDocument(); + }); + + it('shortens long IPNS hosts correctly', () => { + Object.defineProperty(window, 'location', { + value: { + host: 'k2k4r8ng8uzrtqb5ham8kao889m8qezu96z4w3lpinyqghum43veb6n3.ipns.dweb.link', + }, + writable: true, + }); + + render(); + + // Check for the shortened IPNS host in the prompt span + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('k2k4r8ng...43veb6n3.ipns.dweb.link') + ); + }); + expect(promptSpan).toBeInTheDocument(); + }); + + it('shortens long IPFS hosts correctly', () => { + Object.defineProperty(window, 'location', { + value: { + host: 'QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o.ipfs.dweb.link', + }, + writable: true, + }); + + render(); + + // Check for the shortened IPFS host in the prompt span + const promptSpan = screen.getByText((content, element) => { + return ( + element?.tagName.toLowerCase() === 'span' && + content.includes('QmYjtig7...iFofrE7o.ipfs.dweb.link') + ); + }); + expect(promptSpan).toBeInTheDocument(); + }); + + it('handles hosts with port numbers', () => { + Object.defineProperty(window, 'location', { + value: { host: 'localhost:3000' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@localhost:~$'), + ).toBeInTheDocument(); + }); + + it('handles CID shorter than 16 characters', () => { + Object.defineProperty(window, 'location', { + value: { host: 'shortcid.ipfs.dweb.link' }, + writable: true, + }); + + render(); + expect( + screen.getByText('translated_visitor@shortcid.ipfs.dweb.link:~$'), + ).toBeInTheDocument(); + }); + }); });