Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ 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' });

return (
<>
<StaticOutput>
<div>
<p>{t('cmds.services.title')}</p>
<p>{t('cmds.services.description')}</p>
<p>{t('cmds.status.title')}</p>
<p>{t('cmds.status.description')}</p>
</div>
</StaticOutput>
<TerminalEmulator initialCommand={Command.Services} />
<TerminalEmulator initialCommand={Command.Status} />
</>
);
}
20 changes: 8 additions & 12 deletions src/components/about-me/AboutMe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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 (
<div className="flex flex-col gap-(--lsd-spacing-large) py-(--lsd-spacing-small)">
<div className="flex gap-(--lsd-spacing-large) items-center">
Expand Down Expand Up @@ -105,8 +97,12 @@ export default function AboutMe() {
<Typography variant="label1" color="secondary">
{t('gpgLabel')}
</Typography>
<Button variant="ghost" size="square-sm" onClick={handleCopy}>
<CopyIcon weight={copied ? 'fill' : 'duotone'} size={14} />
<Button
variant="ghost"
size="square-sm"
onClick={() => copyWithToast(GPG_FINGERPRINT, t('copyToast'))}
>
<CopyIcon weight={isCopied() ? 'fill' : 'duotone'} size={14} />
</Button>
</div>
<Typography variant="body3" color="secondary">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`}
</Typography>

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

return (
<div className="py-(--lsd-spacing-small)">
<Typography variant="body2">{t('cmds.services.title')}</Typography>
<div className="flex flex-col gap-(--lsd-spacing-smallest)">
<Typography variant="body1">{t('cmds.status.title')}</Typography>
<Typography variant="body2" color="secondary">
{t('cmds.status.subtitle')}
</Typography>
</div>

{isWaitingForHeartbeats ? (
<div className="flex items-center gap-(--lsd-spacing-small) mt-(--lsd-spacing-small)">
<Typography variant="body1" color="secondary">
{isLoading
? t('cmds.services.loading')
: t('cmds.services.waitingForHeartbeats')}
? t('cmds.status.loading')
: t('cmds.status.waitingForHeartbeats')}
</Typography>
</div>
) : (
Expand All @@ -149,7 +154,7 @@ export default function ServicesOutput({ t }: CommandOutputProps) {

<div className="flex flex-col space-y-(--lsd-spacing-smallest)">
<Typography variant="body2" color="secondary">
{t('cmds.services.healthcheckPrefix')}{' '}
{t('cmds.status.healthcheckPrefix')}{' '}
<Button
variant="link"
className="font-bold p-0! h-fit! text-sm!"
Expand All @@ -162,12 +167,12 @@ export default function ServicesOutput({ t }: CommandOutputProps) {
dpulse
</Link>
</Button>
{t('cmds.services.healthcheckSuffix')}
{t('cmds.status.healthcheckSuffix')}
</Typography>

<Typography variant="body2" color="secondary">
<span className="font-bold">dpulse</span>{' '}
{t('cmds.services.dpulseSignsPrefix')}{' '}
{t('cmds.status.dpulseSignsPrefix')}{' '}
<Button
variant="link"
className="font-bold p-0! text-sm! h-fit!"
Expand All @@ -180,7 +185,7 @@ export default function ServicesOutput({ t }: CommandOutputProps) {
Logos Delivery
</Link>
</Button>
{t('cmds.services.logosDeliverySuffix')}
{t('cmds.status.logosDeliverySuffix')}
</Typography>
</div>
</div>
Expand Down
174 changes: 173 additions & 1 deletion src/components/contact/Contact.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>contact / todo</div>;
const t = useTranslations('Contact');
const [email, setEmail] = useState<string | null>(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 (
<div key={labelKey} className="contents">
<Badge
variant="outlined"
size="sm"
className="shrink-0 leading-1"
icon={<Icon weight="duotone" size={14} />}
>
{t(`links.${labelKey}`)}
</Badge>

<Button
variant="ghost"
size="sm"
asChild
className="h-auto py-1 px-2 justify-start"
>
<a
href={href}
target={labelKey === 'email' ? undefined : '_blank'}
rel={labelKey === 'email' ? undefined : 'noopener noreferrer'}
className="justify-start!"
>
<Typography variant="body3">{displayText}</Typography>
</a>
</Button>

{copyable ? (
<Button
variant="ghost"
size="square-sm"
onClick={() => copyWithToast(displayText, t('copyToast'), labelKey)}
aria-label={t('copyLabel')}
>
<CopyIcon
weight={isCopied(labelKey) ? 'fill' : 'duotone'}
size={14}
/>
</Button>
) : (
<div />
)}
</div>
);
};

return (
<div className="flex flex-col gap-(--lsd-spacing-large) py-(--lsd-spacing-small)">
<div className="flex flex-col gap-(--lsd-spacing-smallest)">
<Typography variant="body1">{t('title')}</Typography>
<Typography variant="body2" color="secondary">
{t('subtitle')}
</Typography>
</div>

<div className="grid grid-cols-[auto_auto_auto] w-fit items-center gap-x-(--lsd-spacing-base) gap-y-(--lsd-spacing-smallest)">
{/* Social Section */}
<Typography
variant="body2"
color="secondary"
className="col-span-3 mb-(--lsd-spacing-smaller)"
>
{t('socialSection')}
</Typography>
{socialLinks.map((link) => renderLink(link))}

{/* Direct Contact Section */}
<Typography
variant="body2"
color="secondary"
className="col-span-3 mt-(--lsd-spacing-smaller) mb-(--lsd-spacing-smaller)"
>
{t('directSection')}
</Typography>
{directLinks.map((link) => renderLink(link))}
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,8 +24,8 @@ export const Commands: CommandInfo[] = [
output: ContactOutput,
},
{
name: Command.Services,
output: ServicesOutput,
name: Command.Status,
output: StatusOutput,
},
{
name: Command.Clear,
Expand Down
2 changes: 1 addition & 1 deletion src/constants/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const Routes = {
welcome: '/',
whoami: '/whoami',
services: '/services',
status: '/status',
contact: '/contact',
};
25 changes: 25 additions & 0 deletions src/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState } from 'react';
import { toast } from 'sonner';

export function useCopyToClipboard() {
const [copiedKey, setCopiedKey] = useState<string | null>(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 };
}
Loading
Loading