diff --git a/src/app/[locale]/services/page.tsx b/src/app/[locale]/status/page.tsx similarity index 67% rename from src/app/[locale]/services/page.tsx rename to src/app/[locale]/status/page.tsx index 411b750..52922c6 100644 --- a/src/app/[locale]/services/page.tsx +++ b/src/app/[locale]/status/page.tsx @@ -5,14 +5,14 @@ import type { RouteData } from '@/types/routing'; import { Command } from '@/types/terminal'; import { setPageMeta } from '@/utils/metadata-utils'; -interface ServicesPageProps { +interface StatusPageProps { params: Promise<{ locale: string }>; } export const generateMetadata = async (routeData: RouteData) => - await setPageMeta(routeData, 'services'); + await setPageMeta(routeData, 'status'); -export default async function ServicesPage({ params }: ServicesPageProps) { +export default async function StatusPage({ params }: StatusPageProps) { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Terminal' }); @@ -20,11 +20,11 @@ export default async function ServicesPage({ params }: ServicesPageProps) { <>
-

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

-

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

+

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

+

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

- + ); } diff --git a/src/components/about-me/AboutMe.tsx b/src/components/about-me/AboutMe.tsx index ec8f421..260db2a 100644 --- a/src/components/about-me/AboutMe.tsx +++ b/src/components/about-me/AboutMe.tsx @@ -16,16 +16,15 @@ import { TargetIcon, } from '@phosphor-icons/react'; import { useTranslations } from 'next-intl'; -import { useState } from 'react'; -import { toast } from 'sonner'; import gpgFingerprint from '@/assets/gpg-fingerprint.json'; import profileImage from '@/assets/Pro-Hacked.png'; +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'; const GPG_FINGERPRINT = gpgFingerprint.fingerprint; export default function AboutMe() { const t = useTranslations('AboutMe'); - const [copied, setCopied] = useState(false); + const { copyWithToast, isCopied } = useCopyToClipboard(); const quickInfo = [ { icon: MapPinIcon, textKey: 'badges.location' }, @@ -34,13 +33,6 @@ export default function AboutMe() { { icon: TargetIcon, textKey: 'badges.goal' }, ]; - const handleCopy = async () => { - await navigator.clipboard.writeText(GPG_FINGERPRINT); - setCopied(true); - toast.success(t('copyToast')); - setTimeout(() => setCopied(false), 200); - }; - return (
@@ -105,8 +97,12 @@ export default function AboutMe() { {t('gpgLabel')} -
diff --git a/src/components/cmd-outputs/ServicesOutput.tsx b/src/components/cmd-outputs/StatusOutput.tsx similarity index 87% rename from src/components/cmd-outputs/ServicesOutput.tsx rename to src/components/cmd-outputs/StatusOutput.tsx index f1f2ae6..7ff1f0c 100644 --- a/src/components/cmd-outputs/ServicesOutput.tsx +++ b/src/components/cmd-outputs/StatusOutput.tsx @@ -72,13 +72,13 @@ function ServiceCard({ statusMsg, t, dayjs }: ServiceCardProps) { variant="body1" style={{ color: 'var(--lsd-text-secondary)' }} > - {statusMsg?.description || `${t('cmds.services.noStatusMessage')}`} + {statusMsg?.description || `${t('cmds.status.noStatusMessage')}`}
- {t('cmds.services.lastChecked')}{' '} + {t('cmds.status.lastChecked')}{' '} {dayjs(statusMsg.timestamp).fromNow()}
@@ -88,7 +88,7 @@ function ServiceCard({ statusMsg, t, dayjs }: ServiceCardProps) { ); } -export default function ServicesOutput({ t }: CommandOutputProps) { +export default function StatusOutput({ t }: CommandOutputProps) { const statusMessages = useStore($statusMessages); const isLoading = useStore($isLoading); const dayjs = useDayjs(); @@ -120,14 +120,19 @@ export default function ServicesOutput({ t }: CommandOutputProps) { return (
- {t('cmds.services.title')} +
+ {t('cmds.status.title')} + + {t('cmds.status.subtitle')} + +
{isWaitingForHeartbeats ? (
{isLoading - ? t('cmds.services.loading') - : t('cmds.services.waitingForHeartbeats')} + ? t('cmds.status.loading') + : t('cmds.status.waitingForHeartbeats')}
) : ( @@ -149,7 +154,7 @@ export default function ServicesOutput({ t }: CommandOutputProps) {
- {t('cmds.services.healthcheckPrefix')}{' '} + {t('cmds.status.healthcheckPrefix')}{' '} - {t('cmds.services.healthcheckSuffix')} + {t('cmds.status.healthcheckSuffix')} dpulse{' '} - {t('cmds.services.dpulseSignsPrefix')}{' '} + {t('cmds.status.dpulseSignsPrefix')}{' '} - {t('cmds.services.logosDeliverySuffix')} + {t('cmds.status.logosDeliverySuffix')}
diff --git a/src/components/contact/Contact.tsx b/src/components/contact/Contact.tsx index 65376dc..4e0532b 100644 --- a/src/components/contact/Contact.tsx +++ b/src/components/contact/Contact.tsx @@ -1,3 +1,175 @@ +'use client'; + +import { Badge, Button, Typography } from '@nipsys/lsd'; +import { + ButterflyIcon, + ChatTeardropTextIcon, + CopyIcon, + EnvelopeIcon, + GithubLogoIcon, + type IconWeight, + LinkedinLogoIcon, + PaperPlaneTiltIcon, + TwitterLogoIcon, +} from '@phosphor-icons/react'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; +import { useCopyToClipboard } from '@/hooks/useCopyToClipboard'; + +// Base64 encoded email to prevent scraping (decoded after hydration) +// Generate with: btoa("your@email.com") +const ENCODED_EMAIL = 'Ym9uam91ckB4YXZpZXJzLnNo'; + +interface ContactLink { + icon: React.ComponentType<{ weight?: IconWeight; size?: number }>; + labelKey: string; + href: string; + displayText: string; + copyable?: boolean; +} + export default function Contact() { - return
contact / todo
; + const t = useTranslations('Contact'); + const [email, setEmail] = useState(null); + const { copyWithToast, isCopied } = useCopyToClipboard(); + + useEffect(() => { + setEmail(atob(ENCODED_EMAIL)); + }, []); + + const socialLinks: ContactLink[] = [ + { + icon: GithubLogoIcon, + labelKey: 'github', + href: 'https://github.com/nipsysdev', + displayText: 'nipsysdev', + }, + { + icon: TwitterLogoIcon, + labelKey: 'twitter', + href: 'https://x.com/nipsysdev', + displayText: '@nipsysdev', + }, + { + icon: PaperPlaneTiltIcon, + labelKey: 'telegram', + href: 'https://t.me/nipsysdev', + displayText: '@nipsysdev', + }, + { + icon: ButterflyIcon, + labelKey: 'bluesky', + href: 'https://bsky.app/profile/nipsys.bsky.social', + displayText: '@nipsys.bsky.social', + }, + { + icon: LinkedinLogoIcon, + labelKey: 'linkedin', + href: 'https://linkedin.com/in/xaviersaliniere', + displayText: 'xaviersaliniere', + }, + ]; + + const directLinks: ContactLink[] = [ + { + icon: ChatTeardropTextIcon, + labelKey: 'matrix', + href: 'https://matrix.to/#/@nipsys:nips.im', + displayText: '@nipsys:nips.im', + copyable: true, + }, + { + icon: EnvelopeIcon, + labelKey: 'email', + href: email ? `mailto:${email}` : '#', + displayText: email ?? '...', + copyable: true, + }, + ]; + + const renderLink = ({ + icon: Icon, + labelKey, + href, + displayText, + copyable, + }: ContactLink) => { + return ( +
+ } + > + {t(`links.${labelKey}`)} + + + + + {copyable ? ( + + ) : ( +
+ )} +
+ ); + }; + + return ( +
+
+ {t('title')} + + {t('subtitle')} + +
+ +
+ {/* Social Section */} + + {t('socialSection')} + + {socialLinks.map((link) => renderLink(link))} + + {/* Direct Contact Section */} + + {t('directSection')} + + {directLinks.map((link) => renderLink(link))} +
+
+ ); } diff --git a/src/constants/commands.ts b/src/constants/commands.ts index 01050a2..b6cf07f 100644 --- a/src/constants/commands.ts +++ b/src/constants/commands.ts @@ -1,7 +1,7 @@ import BuildInfoOutput from '@/components/cmd-outputs/BuildInfoOutput'; import ContactOutput from '@/components/cmd-outputs/ContactOutput'; import HelpOutput from '@/components/cmd-outputs/HelpOutput'; -import ServicesOutput from '@/components/cmd-outputs/ServicesOutput'; +import StatusOutput from '@/components/cmd-outputs/StatusOutput'; import WelcomeOutput from '@/components/cmd-outputs/WelcomeOutput'; import WhoamiOutput from '@/components/cmd-outputs/WhoamiOutput'; import { Command, type CommandInfo } from '@/types/terminal'; @@ -24,8 +24,8 @@ export const Commands: CommandInfo[] = [ output: ContactOutput, }, { - name: Command.Services, - output: ServicesOutput, + name: Command.Status, + output: StatusOutput, }, { name: Command.Clear, diff --git a/src/constants/routes.ts b/src/constants/routes.ts index ae41cf6..9db057e 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -1,6 +1,6 @@ export const Routes = { welcome: '/', whoami: '/whoami', - services: '/services', + status: '/status', contact: '/contact', }; diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts new file mode 100644 index 0000000..74863fc --- /dev/null +++ b/src/hooks/useCopyToClipboard.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; + +export function useCopyToClipboard() { + const [copiedKey, setCopiedKey] = useState(null); + + const copy = async (text: string, key: string = 'default') => { + await navigator.clipboard.writeText(text); + setCopiedKey(key); + setTimeout(() => setCopiedKey(null), 200); + }; + + const copyWithToast = async ( + text: string, + toastMessage: string, + key: string = 'default', + ) => { + await copy(text, key); + toast.success(toastMessage); + }; + + const isCopied = (key: string = 'default') => copiedKey === key; + + return { copy, copyWithToast, isCopied }; +} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index c56638f..db95f15 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -15,7 +15,7 @@ "Pages": { "welcome": "welcome", "whoami": "whoami", - "services": "services", + "status": "status", "contact": "contact" }, "Status": { @@ -35,7 +35,7 @@ "andItsUI": "and its UI!" }, "AboutMe": { - "name": "Xav [nipsysdev]", + "name": "Xav [nipsys]", "tagline": "cypherpunk / builder / digital sovereignty advocate", "badges": { "location": "France → Canada → Switzerland", @@ -51,11 +51,28 @@ "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: building AnyMaps and its decentralized infrastructure, contributing to the ecosystems powering my work, getting into cybersecurity, and breaking things to see how they work :)" + "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 :)" }, "gpgLabel": "GPG Fingerprint", "copyToast": "GPG fingerprint copied to clipboard" }, + "Contact": { + "title": "Get in Touch", + "subtitle": "Feel free to reach out for collaboration, questions, or just to say hi!", + "socialSection": "Social", + "directSection": "Direct Contact", + "links": { + "github": "GitHub", + "twitter": "Twitter/X", + "linkedin": "LinkedIn", + "bluesky": "BlueSky", + "telegram": "Telegram", + "matrix": "Matrix", + "email": "Email" + }, + "copyToast": "Copied to clipboard", + "copyLabel": "Copy to clipboard" + }, "Terminal": { "unknownCmdErr": "command not recognized", "noMatch": "no match", @@ -87,9 +104,10 @@ "whoami": { "description": "Allow me to introduce myself" }, - "services": { + "status": { "description": "Display my self-hosted services status", - "title": "Services that I proudly self-host", + "title": "Status", + "subtitle": "Status of the 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 f0528bd..d737577 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -15,7 +15,7 @@ "Pages": { "welcome": "bienvenue", "whoami": "qui suis-je", - "services": "services", + "status": "statut", "contact": "contact" }, "Status": { @@ -35,7 +35,7 @@ "andItsUI": "et sa lib UI !" }, "AboutMe": { - "name": "Xav [nipsysdev]", + "name": "Xav [nipsys]", "tagline": "cypherpunk / développeur / défenseur de la souveraineté numérique", "badges": { "location": "France → Canada → Suisse", @@ -51,11 +51,28 @@ "para3": "Plonger dans Linux, la philosophie FLOSS, l'auto-hébergement de divers services, et progressivement m'extraire 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 : Développement d'AnyMaps et de son infrastructure décentralisée, contribution aux écosystèmes qui alimentent mes projets, formation en cybersécurité, et mes expérimentations routinières :)" + "current": "Ce que je fais actuellement : Me former en cybersécurité, développer AnyMaps et de son infrastructure décentralisée, contribuer aux écosystèmes qui alimentent mes projets, et mes expérimentations routinières :)" }, "gpgLabel": "Empreinte GPG", "copyToast": "Empreinte GPG copiée dans le presse-papier" }, + "Contact": { + "title": "Me contacter", + "subtitle": "N'hésitez pas à me contacter pour collaborer, poser des questions, ou juste pour dire salut !", + "socialSection": "Réseaux sociaux", + "directSection": "Contact direct", + "links": { + "github": "GitHub", + "twitter": "Twitter/X", + "linkedin": "LinkedIn", + "bluesky": "BlueSky", + "telegram": "Telegram", + "matrix": "Matrix", + "email": "Courriel" + }, + "copyToast": "Copié dans le presse-papier", + "copyLabel": "Copier dans le presse-papier" + }, "Terminal": { "unknownCmdErr": "commande non reconnue", "noMatch": "aucune correspondance", @@ -87,9 +104,10 @@ "whoami": { "description": "Permettez-moi de me présenter" }, - "services": { + "status": { "description": "Afficher l'état de mes services auto-hébergés", - "title": "Services que j'héberge avec fierté", + "title": "Statut", + "subtitle": "Statut des services que je suis fier d'héberger", "connectionStatus": "État de la connexion", "loading": "(Chargement...)", "waitingForHeartbeats": "En attente de heartbeats...", diff --git a/src/lib/dpulse/utils/status.ts b/src/lib/dpulse/utils/status.ts index a84e816..c6f1680 100644 --- a/src/lib/dpulse/utils/status.ts +++ b/src/lib/dpulse/utils/status.ts @@ -12,19 +12,19 @@ export const STATUS_META = { healthy: { variant: 'success' as const, icon: CheckCircleIcon, - textKey: 'cmds.services.status.healthy', + textKey: 'cmds.status.status.healthy', className: '', }, degraded: { variant: 'warning' as const, icon: CircleNotchIcon, - textKey: 'cmds.services.status.degraded', + textKey: 'cmds.status.status.degraded', className: 'animate-spin', }, down: { variant: 'destructive' as const, icon: XCircleIcon, - textKey: 'cmds.services.status.down', + textKey: 'cmds.status.status.down', className: '', }, }; diff --git a/src/types/terminal.ts b/src/types/terminal.ts index c7a838f..102c477 100644 --- a/src/types/terminal.ts +++ b/src/types/terminal.ts @@ -6,7 +6,7 @@ export enum Command { Clear = 'clear', Contact = 'contact', Help = 'help', - Services = 'services', + Status = 'status', Welcome = 'welcome', Whoami = 'whoami', }