diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5063ad0..b6a60bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,7 +179,7 @@ jobs: IPNS_NAME=$(echo "$PUBLISH_RESP" | grep -oE 'Published to [a-zA-Z0-9]+' | awk '{print $3}') echo "IPNS Name: $IPNS_NAME" echo "IPNS published successfully!" - echo "Site available at: https://nipsys.eth.limo" + echo "Site available at: https://xav.gwei.site" - name: Pin to 4EVERLand env: @@ -247,4 +247,4 @@ jobs: done echo "⚠️ TIMEOUT: Pin did not reach 'pinned' status within 60 seconds" - exit 1 \ No newline at end of file + exit 1 diff --git a/.gitmodules b/.gitmodules index ab1e027..2ff6935 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "external/resume"] path = external/resume - url = git@github.com:nipsysdev/resume.git + url = https://github.com/nipsysdev/resume.git diff --git a/README.md b/README.md index dc98216..a25ff45 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# nipsys.dev +# xav.gwei.site My personal portfolio website with a terminal-style interface. Navigate using CLI commands instead of traditional UI. ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue) ![Node](https://img.shields.io/badge/node-%3E%3D22.0.0-green) -![pnpm](https://img.shields.io/badge/pnpm-%3E%3D9-orange) +![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-orange) -**Live:** [nipsys.eth.limo](https://nipsys.eth.limo) +**Live:** [xav.gwei.site](https://xav.gwei.site) ## What makes it unique @@ -15,7 +15,7 @@ CLI commands instead of traditional UI. `contact` - **Peer-to-peer data retrieval** — Real-time service health and recent Pixelfed posts fetch via Logos Delivery (A censorship resistant P2P messaging network) -- **IPFS hosted** — Deployed on IPFS with IPNS, accessible via ENS +- **IPFS hosted** — Deployed on IPFS with IPNS ## Commands @@ -44,7 +44,7 @@ CLI commands instead of traditional UI. ## Getting Started -**Requirements:** Node.js ≥22, pnpm ≥9 +**Requirements:** Node.js ≥22, pnpm ≥10 ```bash git clone https://github.com/nipsysdev/site.git diff --git a/external/resume b/external/resume index 40bdfde..d1c4b52 160000 --- a/external/resume +++ b/external/resume @@ -1 +1 @@ -Subproject commit 40bdfde2d2461bb0d530d84c32cc1a7531d7b344 +Subproject commit d1c4b52a9b686770034d26ca02cd1177266c31aa diff --git a/next.config.ts b/next.config.ts index 34c0084..1f116bc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process'; import type { NextConfig } from 'next'; import createNextIntlPlugin from 'next-intl/plugin'; @@ -5,6 +6,14 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); +function getGitCommit(): string { + try { + return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + } catch { + return 'unknown'; + } +} + const nextConfig: NextConfig = { devIndicators: false, output: 'export', @@ -15,6 +24,7 @@ const nextConfig: NextConfig = { }, env: { BUILD_TIMESTAMP: new Date().toISOString(), + BUILD_COMMIT: getGitCommit(), IPNS_HASH: process.env.IPNS_HASH || 'k2k4r8ng8uzrtqb5ham8kao889m8qezu96z4w3lpinyqghum43veb6n3', diff --git a/package.json b/package.json index 28b084d..1a85373 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "type": "git", "url": "git+https://github.com/nipsysdev/site" }, - "homepage": "https://nipsys.dev/", + "homepage": "https://xav.gwei.site", "author": "Xavier Saliniere ", "engines": { "node": ">=22.0.0", - "pnpm": ">=9" + "pnpm": ">=10" }, "scripts": { "copy-resume": "node scripts/copy-resume.js", @@ -38,7 +38,6 @@ "@phosphor-icons/react": "^2.1.7", "@waku/sdk": "^0.0.36", "dayjs": "^1.11.20", - "fast-xml-parser": "^5.8.0", "nanostores": "^1.1.1", "next": "^15.5.12", "next-intl": "^4.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1988bf..463cc67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: dayjs: specifier: ^1.11.20 version: 1.11.20 - fast-xml-parser: - specifier: ^5.8.0 - version: 5.8.0 nanostores: specifier: ^1.1.1 version: 1.3.0 @@ -1011,9 +1008,6 @@ packages: '@noble/secp256k1@1.7.2': resolution: {integrity: sha512-/qzwYl5eFLH8OWIecQWM31qld2g1NfjgylK+TNhqtaUKP37Nm+Y+z30Fjhw0Ct8p9yCQEm2N3W/AckdIb3SMcQ==} - '@nodable/entities@2.1.1': - resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3071,13 +3065,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-xml-builder@1.2.0: - resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - - fast-xml-parser@5.8.0: - resolution: {integrity: sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==} - hasBin: true - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -4106,10 +4093,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -4524,9 +4507,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.3.0: - resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} - strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -4957,10 +4937,6 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml-naming@0.1.0: - resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} - engines: {node: '>=16.0.0'} - xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -6408,8 +6384,6 @@ snapshots: '@noble/secp256k1@1.7.2': {} - '@nodable/entities@2.1.1': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8518,19 +8492,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-xml-builder@1.2.0: - dependencies: - path-expression-matcher: 1.5.0 - xml-naming: 0.1.0 - - fast-xml-parser@5.8.0: - dependencies: - '@nodable/entities': 2.1.1 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.3.0 - xml-naming: 0.1.0 - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -9777,8 +9738,6 @@ snapshots: parseurl@1.3.3: {} - path-expression-matcher@1.5.0: {} - path-key@3.1.1: {} path-scurry@1.11.1: @@ -10342,8 +10301,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.3.0: {} - strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -10743,8 +10700,6 @@ snapshots: xml-name-validator@5.0.0: {} - xml-naming@0.1.0: {} - xml2js@0.6.2: dependencies: sax: 1.6.0 diff --git a/src/__tests__/utils/terminal-utils.test.ts b/src/__tests__/utils/terminal-utils.test.ts index 3f57421..2b9b14b 100644 --- a/src/__tests__/utils/terminal-utils.test.ts +++ b/src/__tests__/utils/terminal-utils.test.ts @@ -322,48 +322,48 @@ describe('terminal-utils', () => { it('returns full host for regular domains', () => { vi.stubGlobal('window', { - location: { host: 'nipsys.dev:443' }, + location: { host: 'xav.gwei.site:443' }, }); - expect(getDisplayHost()).toBe('nipsys.dev'); + expect(getDisplayHost()).toBe('xav.gwei.site'); }); it('returns host without port when no port specified', () => { vi.stubGlobal('window', { - location: { host: 'nipsys.dev' }, + location: { host: 'xav.gwei.site' }, }); - expect(getDisplayHost()).toBe('nipsys.dev'); + expect(getDisplayHost()).toBe('xav.gwei.site'); }); it('strips first part from IPNS hosts', () => { vi.stubGlobal('window', { location: { - host: 'k51qzi5uqu5dkkciu33khkzbcmxtyhn2i3v.ipns.nipsys.dev', + host: 'k51qzi5uqu5dkkciu33khkzbcmxtyhn2i3v.ipns.xav.gwei.site', }, }); - expect(getDisplayHost()).toBe('ipns.nipsys.dev'); + expect(getDisplayHost()).toBe('ipns.xav.gwei.site'); }); it('strips first part from IPFS hosts', () => { vi.stubGlobal('window', { location: { - host: 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.ipfs.nipsys.dev', + host: 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.ipfs.xav.gwei.site', }, }); - expect(getDisplayHost()).toBe('ipfs.nipsys.dev'); + expect(getDisplayHost()).toBe('ipfs.xav.gwei.site'); }); it('returns full host when IPNS/IPFS but no subdomain parts', () => { vi.stubGlobal('window', { location: { - host: 'ipns.nipsys.dev', + host: 'ipns.xav.gwei.site', }, }); - expect(getDisplayHost()).toBe('nipsys.dev'); + expect(getDisplayHost()).toBe('xav.gwei.site'); }); it('handles localhost with port', () => { diff --git a/src/assets/Pro-Hacked.webp b/src/assets/Pro-Hacked.webp deleted file mode 100644 index da0f175..0000000 Binary files a/src/assets/Pro-Hacked.webp and /dev/null differ diff --git a/src/assets/me.webp b/src/assets/me.webp new file mode 100644 index 0000000..f630862 Binary files /dev/null and b/src/assets/me.webp differ diff --git a/src/assets/nipsys.webp b/src/assets/nipsys.webp index a93f80d..c1e9fbd 100644 Binary files a/src/assets/nipsys.webp and b/src/assets/nipsys.webp differ diff --git a/src/components/cmd-outputs/BuildInfoOutput.tsx b/src/components/cmd-outputs/BuildInfoOutput.tsx index dc1005d..ff58038 100644 --- a/src/components/cmd-outputs/BuildInfoOutput.tsx +++ b/src/components/cmd-outputs/BuildInfoOutput.tsx @@ -1,23 +1,146 @@ +'use client'; + +import { Button, Typography } from '@nipsys/lsd'; +import Link from 'next/link'; import { useTranslations } from 'next-intl'; +import { Fragment, type JSX } from 'react'; +import { useIpnsCid } from '@/hooks/useIpnsCid'; + +const IPNS_HASH = process.env.IPNS_HASH; +const REPO_URL = 'https://github.com/nipsysdev/site'; + +interface NamedGateway { + label: string; + url: string; +} + +interface PublicGateway { + label: string; + ipnsUrl: string; + cidUrlPattern: (cid: string) => string; +} export default function BuildInfoOutput() { const t = useTranslations('BuildInfo'); const buildTimestamp = process.env.BUILD_TIMESTAMP; - const ipnsHash = process.env.IPNS_HASH; + const buildCommit = process.env.BUILD_COMMIT; + const { cid, isLoading } = useIpnsCid(IPNS_HASH); + + const namedGateways: NamedGateway[] = [ + { + label: 'gwei.site', + url: 'https://xav.gwei.site', + }, + { + label: 'eth.limo', + url: 'https://nipsys.eth.limo', + }, + ]; + + const publicGateways: PublicGateway[] = [ + { + label: 'dweb.link', + ipnsUrl: IPNS_HASH ? `https://${IPNS_HASH}.ipns.dweb.link` : '#', + cidUrlPattern: (resolvedCid) => `https://${resolvedCid}.ipfs.dweb.link`, + }, + { + label: 'ipfs.io', + ipnsUrl: IPNS_HASH ? `https://ipfs.io/ipns/${IPNS_HASH}` : '#', + cidUrlPattern: (resolvedCid) => `https://ipfs.io/ipfs/${resolvedCid}`, + }, + ]; + + const cell = (title: string, content?: JSX.Element | string) => ( +
+ + {title} + + {content && {content}} +
+ ); return ( -
-
{t('title')}
-
-
- {t('timeLabel')}{' '} - {buildTimestamp +
+
+ {cell( + t('buildTimeLabel'), + buildTimestamp ? new Date(buildTimestamp).toLocaleString() - : t('unknown')} -
-
- {t('ipnsLabel')} {ipnsHash || t('notConfigured')} + : t('unknown'), + )} + {cell( + t('commitLabel'), + buildCommit && buildCommit !== 'unknown' ? ( + + ) : ( + t('unknown') + ), + )} +
+ {cell( + t('ipfsCid'), + isLoading ? t('cidResolving') : cid ? cid : t('cidUnavailable'), + )}
+ {IPNS_HASH && ( +
{cell(t('ipnsName'), IPNS_HASH)}
+ )} + +
{cell(t('gateways'))}
+ + {namedGateways.map((g) => ( + + {cell(g.label)} + + + ))} + + {publicGateways.map((g) => ( + + {cell(g.label)} +
+ {cid && ( + + )} + +
+
+ ))}
); diff --git a/src/components/cmd-outputs/GalleryOutput.tsx b/src/components/cmd-outputs/GalleryOutput.tsx index b8a26bd..dd2d6b9 100644 --- a/src/components/cmd-outputs/GalleryOutput.tsx +++ b/src/components/cmd-outputs/GalleryOutput.tsx @@ -238,7 +238,7 @@ function PostCard({ entry, t }: { entry: FeedEntry; t: Translator }) { size="square-md" onClick={handlePreviousImage} className="absolute left-(--lsd-spacing-smaller) bg-(--lsd-primary-content)!" - aria-label="Previous image" + aria-label={t('previousImage')} > @@ -247,7 +247,7 @@ function PostCard({ entry, t }: { entry: FeedEntry; t: Translator }) { size="square-md" onClick={handleNextImage} className="absolute right-(--lsd-spacing-smaller) bg-(--lsd-primary-content)!" - aria-label="Next image" + aria-label={t('nextImage')} > diff --git a/src/components/cmd-outputs/WhoamiOutput.tsx b/src/components/cmd-outputs/WhoamiOutput.tsx index 38d3b39..0813aca 100644 --- a/src/components/cmd-outputs/WhoamiOutput.tsx +++ b/src/components/cmd-outputs/WhoamiOutput.tsx @@ -17,7 +17,7 @@ import { } from '@phosphor-icons/react'; import { useTranslations } from 'next-intl'; import gpgFingerprint from '@/assets/gpg-fingerprint.json'; -import profileImage from '@/assets/Pro-Hacked.webp'; +import profileImage from '@/assets/me.webp'; import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'; const GPG_FINGERPRINT = gpgFingerprint.fingerprint; @@ -71,6 +71,7 @@ export default function WhoamiOutput() {
{tAbout('bio.intro1')} {tAbout('bio.intro2')} + {tAbout('bio.intro3')} {tAbout('bio.para1')} @@ -88,10 +89,6 @@ export default function WhoamiOutput() { {tAbout('bio.para4')} - - {tAbout('bio.para5')} - - {tAbout('bio.current')}
diff --git a/src/hooks/useIpnsCid.ts b/src/hooks/useIpnsCid.ts new file mode 100644 index 0000000..1b04af5 --- /dev/null +++ b/src/hooks/useIpnsCid.ts @@ -0,0 +1,66 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface UseIpnsCidResult { + cid: string | null; + isLoading: boolean; + error: string | null; +} + +/** + * Resolves an IPNS name to its current IPFS CID client-side. + * + * Uses my personal gateway and reads the `x-ipfs-roots` response + * header (CORS-exposed) to extract the resolved root CID. + */ +export function useIpnsCid(ipnsName: string | undefined): UseIpnsCidResult { + const [cid, setCid] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!ipnsName) return; + + let cancelled = false; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + setIsLoading(true); + setError(null); + + fetch(`https://ipfs.nipsys.dev/ipns/${ipnsName}/`, { + method: 'GET', + signal: controller.signal, + }) + .then((res) => { + const roots = res.headers.get('x-ipfs-roots'); + if (!roots) { + throw new Error('No x-ipfs-roots header in response'); + } + // x-ipfs-roots is comma-separated; first entry is the resolved root CID + const resolvedCid = roots.split(',')[0].trim(); + if (!cancelled) { + setCid(resolvedCid); + setIsLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err instanceof Error ? err.message : 'Resolution failed'); + setIsLoading(false); + } + }) + .finally(() => { + clearTimeout(timeout); + }); + + return () => { + cancelled = true; + clearTimeout(timeout); + controller.abort(); + }; + }, [ipnsName]); + + return { cid, isLoading, error }; +} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7fd6fc9..f88ef2f 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1,7 +1,7 @@ { "Metadata": { - "title": "xav/nipsys.eth", - "description": "My personal website" + "title": "Xavier Saliniere", + "description": "My personal and decentralized website" }, "Core": { "menu": "menu", @@ -37,23 +37,23 @@ "andItsUI": "and its UI!" }, "AboutMe": { - "name": "Xav [nipsys]", - "tagline": "cypherpunk / builder / digital sovereignty advocate", + "name": "Xavier Saliniere", + "tagline": "cypherpunk / builder / digital freedom", "badges": { "location": "France → Canada → Switzerland", "experience": "10+ yrs building for the web", "focus": "Privacy, Decentralization, FLOSS", - "goal": "Aspiring penetration tester" + "goal": "Building alternative infrastructure" }, "bio": { - "intro1": "My name is Xavier Saliniere — a cypherpunk at heart, building and breaking things for a freer internet.", - "intro2": "I'm driven by a simple question: what if the internet actually worked for the people using it?", - "para1": "I started coding at 14, hacking together PSP homebrews in my bedroom. What began as teenage curiosity evolved into a decade-long journey through the web development world — from dropping out of school at 21 to join a startup, to building enterprise security platforms at Bell Canada's Operations Center.", + "intro1": "My name is Xavier Saliniere — a cypherpunk at heart, building and experimenting for a freer internet.", + "intro2": "I'm driven by a simple question: what if the internet and the code running on it actually worked for its users?", + "intro3": "Like Bitcoin and Ethereum, I believe decentralized, privacy-respecting, censorship-resistant systems without intermediaries are key to a better internet.", + "para1": "I started coding at 14, hacking together PSP homebrews in my bedroom. What began as teenage curiosity evolved into a decade-long journey through the web development world — from joining a real estate startup at 21, to building enterprise security platforms at Bell Canada's Operations Center.", "para2": "But the real story isn't in the job titles. It's in the late nights experimenting and learning about alternative systems and technologies.", "para3": "Diving into Linux, the FLOSS philosophy, self-hosting various services, and gradually extricating myself from the walled gardens of Big Tech. The more I learned about centralized systems and the organizations behind them, the more I developed a passion for their alternatives.", "para4": "After 4 years in Canada, I'm now heading to Switzerland — bringing my passion for decentralized, permissionless, and privacy-preserving technologies with me.", - "para5": "Career-wise, following 10 years in web development, I'm working on shifting towards cybersecurity, with a strong interest in penetration testing.", - "current": "Currently focused on: Getting into cybersecurity, building AnyMaps and its decentralized infrastructure, contributing to the ecosystems powering my work and breaking things to see how they work :)" + "current": "Currently: Building software for companies while developing AnyMaps, an open-source and offline-first mapping app that leverages decentralized technologies." }, "gpgLabel": "GPG Fingerprint", "copyToast": "GPG fingerprint copied to clipboard" @@ -117,9 +117,13 @@ "siteIntro2": "Or, for a CLI experience... Get started with ." }, "BuildInfo": { - "title": "Build Information", - "timeLabel": "Build Time:", - "ipnsLabel": "IPNS Name:", + "buildTimeLabel": "Build Time", + "commitLabel": "Commit", + "ipfsCid": "IPFS CID", + "ipnsName": "IPNS Name", + "gateways": "Gateways", + "cidResolving": "Resolving…", + "cidUnavailable": "CID unavailable", "unknown": "Unknown", "notConfigured": "Not configured" }, @@ -167,6 +171,8 @@ "feedPrefix": "Pixelfed feed is provided by", "feedSuffix": ". Which runs as a systemd service on a VPS.", "dpulseSignsPrefix": "retrieves, signs, and publishes the feed which is then retrieved by this static site via", - "logosDeliverySuffix": ", a peer-to-peer & censorship-resistant messaging network." + "logosDeliverySuffix": ", a peer-to-peer & censorship-resistant messaging network.", + "previousImage": "Previous image", + "nextImage": "Next image" } } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 048f225..6f442dd 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1,7 +1,7 @@ { "Metadata": { - "title": "xav/nipsys.eth", - "description": "Mon site personnel" + "title": "Xavier Saliniere", + "description": "Mon site personnel et décentralisé" }, "Core": { "menu": "menu", @@ -37,23 +37,23 @@ "andItsUI": "et sa lib UI !" }, "AboutMe": { - "name": "Xav [nipsys]", - "tagline": "cypherpunk / développeur / militant pour la souveraineté numérique", + "name": "Xavier Saliniere", + "tagline": "cypherpunk / développeur / liberté numérique", "badges": { "location": "France → Canada → Suisse", - "experience": "10+ ans à construire sur la toile", + "experience": "10+ ans à construire sur le web", "focus": "Confidentialité, Décentralisation, FLOSS", - "goal": "Pentester en devenir" + "goal": "Construire des infrastructures alternatives" }, "bio": { "intro1": "Je m'appelle Xavier Saliniere — cypherpunk dans l'âme, je développe et j'expérimente pour un internet plus libre.", - "intro2": "Une simple question me motive : et si l'internet pouvait réellement être au service de ceux qui l'utilisent ?", - "para1": "J'ai commencé à coder à 14 ans, en bricolant des homebrews PSP dans ma chambre. Ce qui a commencé comme une curiosité d'adolescent s'est transformé en un voyage d'une décennie à travers le monde du développement web — depuis l'abandon de mes études à 21 ans pour rejoindre une startup, jusqu'à développer des plateformes de sécurité d'entreprise au Centre des Opérations de Bell Canada.", + "intro2": "Une simple question me motive : et si l'internet et le code qui y tourne pouvaient réellement être au service de leurs utilisateurs ?", + "intro3": "À l'instar de Bitcoin et Ethereum, je crois que des systèmes décentralisés, sans tiers de confiance, respectueux de la vie privée et résistants à la censure sont la clef pour un meilleur internet.", + "para1": "J'ai commencé à coder à 14 ans, en bricolant des homebrews PSP dans ma chambre. Ce qui a commencé comme une curiosité d'adolescent s'est transformé en un voyage d'une décennie à travers le monde du développement web — depuis mes débuts dans une startup immobilière à 21 ans, jusqu'à développer des plateformes de sécurité d'entreprise au Centre des Opérations de Bell Canada.", "para2": "Mais la vraie histoire n'est pas dans les postes que j'ai occupé. Elle est dans les nuits blanches à expérimenter et à découvrir des systèmes et technologies alternatifs.", - "para3": "Je me suis plongé dans Linux, la philosophie FLOSS, l'auto-hébergement de divers services, et progressivement je me suis extrait des \"walled gardens\" de la Big Tech. Plus j'apprenais sur les systèmes centralisés et les organisations qui les contrôlent, plus je suis devenu passioné par leurs alternatives.", - "para4": "Après 4 ans au Canada, je m'installe maintenant en Suisse — emportant avec moi ma passion pour les technologies décentralisées, \"permissionless\" et respectueuses de la vie privée.", - "para5": "Côté carrière, après 10 ans en développement web, je me forme pour transitionner vers la cybersécurité, avec un fort intérêt pour le pentesting.", - "current": "Ce que je fais actuellement : Me former en cybersécurité, développer AnyMaps et son infrastructure décentralisée, contribuer aux écosystèmes qui alimentent mes projets, et mes expérimentations routinières :)" + "para3": "Je me suis plongé dans Linux, la philosophie FLOSS, l'auto-hébergement de divers services, et progressivement je me suis extrait des \"walled gardens\" de la Big Tech. Plus j'apprenais sur les systèmes centralisés et les organisations qui les contrôlent, plus je suis devenu passionné par leurs alternatives.", + "para4": "Après 4 ans au Canada, la Suisse est ma prochaine étape — emportant avec moi ma passion pour les technologies décentralisées, \"permissionless\" et respectueuses de la vie privée.", + "current": "Actuellement : Je continue de développer des logiciels pour des entreprises, tout en construisant AnyMaps, une application de cartographie open-source et offline-first qui s'appuie sur des technologies décentralisées." }, "gpgLabel": "Empreinte GPG", "copyToast": "Empreinte GPG copiée dans le presse-papier" @@ -117,9 +117,13 @@ "siteIntro2": "Ou, pour une expérience CLI... Vous pouvez commencer avec ." }, "BuildInfo": { - "title": "Informations du build", - "timeLabel": "Date du build :", - "ipnsLabel": "Nom IPNS :", + "buildTimeLabel": "Date du build", + "commitLabel": "Commit", + "ipfsCid": "IPFS CID", + "ipnsName": "Nom IPNS", + "gateways": "Passerelles", + "cidResolving": "Résolution…", + "cidUnavailable": "CID indisponible", "unknown": "Inconnu", "notConfigured": "Non configuré" }, @@ -167,6 +171,8 @@ "feedPrefix": "Le flux Pixelfed est fourni par", "feedSuffix": ". Qui tourne en tant que service systemd sur un VPS.", "dpulseSignsPrefix": "récupère, signe et publie le flux qui est ensuite récupéré par ce site statique via", - "logosDeliverySuffix": ", un réseau de messagerie pair-à-pair et résistant à la censure." + "logosDeliverySuffix": ", un réseau de messagerie pair-à-pair et résistant à la censure.", + "previousImage": "Image précédente", + "nextImage": "Image suivante" } } diff --git a/src/lib/dpulse/config.ts b/src/lib/dpulse/config.ts index 20f4af9..22448b5 100644 --- a/src/lib/dpulse/config.ts +++ b/src/lib/dpulse/config.ts @@ -4,5 +4,5 @@ export const DPULSE_CONFIG = { publicKey: `-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAt2CsQmm+c5L0UQmpkowqQ1WqTcFHsv6kZhI508t1sXU= -----END PUBLIC KEY-----`, - userAgent: 'dev.nipsys.site', + userAgent: 'dpulse.xav.gwei.site', } as const; diff --git a/src/lib/pixelfed.ts b/src/lib/pixelfed.ts deleted file mode 100644 index 786e99c..0000000 --- a/src/lib/pixelfed.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { XMLParser } from 'fast-xml-parser'; - -const FEED_URL = 'https://pixelfed.social/users/xaviers.atom'; - -const parser = new XMLParser({ - ignoreAttributes: false, - attributeNamePrefix: '@_', -}); - -export interface PixelFedFeed { - id: string; - title: string; - subtitle: string; - updated: string; - author: { - name: string; - uri: string; - }; - icon: string; - entries: PixelFedEntry[]; -} - -export interface PixelFedEntry { - id: string; - title: string; - summary: string; - content: string; - updated: string; - link: string; - images: PixelFedImage[]; -} - -export interface PixelFedImage { - url: string; - type: string; - alt?: string; -} - -interface RawAtomLink { - '@_href': string; - '@_rel'?: string; -} - -interface RawMediaContent { - '@_url': string; - '@_type': string; - '@_medium': string; -} - -interface RawAtomEntry { - id: string; - title: string; - summary?: string; - content?: string; - updated: string; - link: RawAtomLink | RawAtomLink[]; - 'media:content': RawMediaContent | RawMediaContent[]; -} - -export async function fetchPixelFedFeed(): Promise { - const response = await fetch(FEED_URL, { - next: { revalidate: 3600 }, - }); - - if (!response.ok) { - throw new Error(`Failed to fetch PixelFed feed: ${response.status}`); - } - - return response.text(); -} - -export function parsePixelFedFeed(xml: string): PixelFedFeed { - const parsed = parser.parse(xml); - const feed = parsed.feed; - - const rawEntries = feed.entry; - const entriesArray: RawAtomEntry[] = Array.isArray(rawEntries) - ? rawEntries - : rawEntries - ? [rawEntries] - : []; - - const entries: PixelFedEntry[] = entriesArray.map((entry: RawAtomEntry) => ({ - id: entry.id, - title: entry.title, - summary: entry.summary || '', - content: entry.content || '', - updated: entry.updated, - link: extractLink(entry.link), - images: extractImages(entry['media:content']), - })); - - return { - id: feed.id, - title: feed.title, - subtitle: feed.subtitle || '', - updated: feed.updated, - author: { - name: feed.author.name, - uri: feed.author.uri, - }, - icon: feed.icon || '', - entries, - }; -} - -export function extractLink(link: RawAtomLink | RawAtomLink[]): string { - if (!link) return ''; - if (Array.isArray(link)) { - const selfLink = link.find( - (l) => l['@_rel'] === 'self' || l['@_rel'] === 'alternate', - ); - return selfLink?.['@_href'] || link[0]?.['@_href'] || ''; - } - return link['@_href'] || ''; -} - -export function extractImages( - mediaContent: RawMediaContent | RawMediaContent[], -): PixelFedImage[] { - if (!mediaContent) return []; - - const items = Array.isArray(mediaContent) ? mediaContent : [mediaContent]; - - return items - .filter((item: RawMediaContent) => item['@_medium'] === 'image') - .map((item: RawMediaContent) => ({ - url: item['@_url'], - type: item['@_type'], - })); -} diff --git a/tsconfig.json b/tsconfig.json index c133409..55e2a02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2017", + "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true,