Skip to content
Open
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 @@ -19,9 +19,8 @@ import {
import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
import { Input } from '@/components/ui/input';
import { LabelWithTooltip } from '@/components/ui/label-with-tooltip';
import { ShareButton } from '@/components/ui/share-button';
import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { TooltipProvider } from '@/components/ui/tooltip';
import { MultiSelect } from '@/components/ui/multi-select';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
Expand Down Expand Up @@ -420,13 +419,7 @@ export default function ThroughputCalculatorDisplay() {
across all GPUs. Values are interpolated from real benchmark data.
</p>
</div>
<div className="flex items-center gap-1.5">
<ShareButton />
<div className="hidden sm:flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
</div>
<ChartShareActions />
</div>

{/* Controls — grid layout matching inference chart controls */}
Expand Down
9 changes: 2 additions & 7 deletions packages/app/src/components/gpu-power/GpuPowerDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import ChartLegend from '@/components/ui/chart-legend';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
import { ShareButton } from '@/components/ui/share-button';
import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import {
Select,
SelectContent,
Expand Down Expand Up @@ -259,11 +258,7 @@ export default function GpuMetricsDisplay() {
<Lock className="size-3" />
Re-lock feature gate
</Button>
<ShareButton />
<div className="hidden sm:flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
<ChartShareActions />
</div>
</div>
<div className="flex flex-wrap items-end gap-3">
Expand Down
11 changes: 2 additions & 9 deletions packages/app/src/components/gpu-specs/gpu-specs-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { BarChart3, Radar, Table2 } from 'lucide-react';

import { Card } from '@/components/ui/card';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
import { ShareButton } from '@/components/ui/share-button';
import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { UnofficialDomainNotice } from '@/components/ui/unofficial-domain-notice';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import {
formatTflops,
getScaleUpDomainMemory,
Expand Down Expand Up @@ -321,13 +320,7 @@ export function GpuSpecsContent() {
compute performance, memory bandwidth, and interconnect details.
</p>
</div>
<div className="flex items-center gap-1.5">
<ShareButton />
<div className="hidden sm:flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
</div>
<ChartShareActions />
</div>
</Card>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ChartButtons } from '@/components/ui/chart-buttons';
import { SegmentedToggle, type SegmentedToggleOption } from '@/components/ui/segmented-toggle';
import { ShareButton } from '@/components/ui/share-button';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { exportToCsv } from '@/lib/csv-export';
import { submissionsVolumeToCsv } from '@/lib/csv-export-helpers';
import { useSubmissions } from '@/hooks/api/use-submissions';
Expand Down Expand Up @@ -90,11 +89,7 @@ export default function SubmissionsDisplay() {
<Lock className="size-3" />
Re-lock feature gate
</Button>
<ShareButton />
<div className="hidden sm:flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
<ChartShareActions />
</div>
</div>
</Card>
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/components/ui/chart-display-helpers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ afterEach(() => {
});

describe('ChartShareActions', () => {
it('renders the shared copy-link, X, and LinkedIn buttons with stable test ids', () => {
it('renders the share popover trigger', () => {
renderUi(<ChartShareActions />);

expect(container.querySelector('[data-testid="share-button"]')).not.toBeNull();
expect(container.querySelector('[data-testid="share-twitter"]')).not.toBeNull();
expect(container.querySelector('[data-testid="share-linkedin"]')).not.toBeNull();
const trigger = container.querySelector('[data-testid="share-button"]');
expect(trigger).not.toBeNull();
expect(trigger?.textContent).toContain('Share');
});
});

Expand Down
11 changes: 1 addition & 10 deletions packages/app/src/components/ui/chart-display-helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Link from 'next/link';
import type { ReactNode } from 'react';

import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { Badge } from '@/components/ui/badge';
import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
import { ShareButton } from '@/components/ui/share-button';
Expand Down Expand Up @@ -93,15 +92,7 @@ function getCostValues(selectedYAxisMetric: string) {
}

export function ChartShareActions() {
return (
<div className="flex items-center gap-1.5">
<ShareButton />
<div className="hidden sm:flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
</div>
);
return <ShareButton />;
}

export function MetricAssumptionNotes({
Expand Down
62 changes: 62 additions & 0 deletions packages/app/src/components/ui/share-button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @vitest-environment jsdom
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('@/lib/url-state', () => ({
buildShareUrl: () => 'https://inferencex.semianalysis.com/?g_model=dsr1#inference',
}));

vi.mock('@/lib/analytics', () => ({
track: vi.fn(),
}));

import { ShareButton } from '@/components/ui/share-button';

let container: HTMLDivElement;
let root: Root;

function renderUi(ui: React.ReactNode) {
act(() => root.render(ui));
}

beforeEach(() => {
container = document.createElement('div');
document.body.append(container);
root = createRoot(container);
});

afterEach(() => {
act(() => root.unmount());
container.remove();
});

describe('ShareButton', () => {
it('renders a closed popover trigger by default', () => {
renderUi(<ShareButton />);

const trigger = container.querySelector('[data-testid="share-button"]');
expect(trigger).not.toBeNull();
expect(trigger?.textContent).toContain('Share');
// Popover content lives in a portal and is not in the DOM until opened.
expect(document.querySelector('[data-testid="share-popover"]')).toBeNull();
});

it('opens the popover with the share URL pre-filled when the trigger is clicked', () => {
renderUi(<ShareButton />);

const trigger = container.querySelector<HTMLButtonElement>('[data-testid="share-button"]');
expect(trigger).not.toBeNull();

act(() => trigger?.click());

const input = document.querySelector<HTMLInputElement>('[data-testid="share-url-input"]');
expect(input).not.toBeNull();
expect(input?.value).toBe('https://inferencex.semianalysis.com/?g_model=dsr1#inference');

// Copy + social buttons live inside the popover content.
expect(document.querySelector('[data-testid="share-copy-button"]')).not.toBeNull();
expect(document.querySelector('[data-testid="share-twitter"]')).not.toBeNull();
expect(document.querySelector('[data-testid="share-linkedin"]')).not.toBeNull();
});
});
127 changes: 95 additions & 32 deletions packages/app/src/components/ui/share-button.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,123 @@
'use client';

import { track } from '@/lib/analytics';
import { Check, Link as LinkIcon } from 'lucide-react';
import { useCallback, useState } from 'react';
import { Check, Copy, Share2 } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';

import { ShareLinkedInButton, ShareTwitterButton } from '@/components/share-buttons';
import { track } from '@/lib/analytics';
import { buildShareUrl } from '@/lib/url-state';

import { Button } from './button';
import { Popover, PopoverContent, PopoverTrigger } from './popover';

export function ShareButton() {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [url, setUrl] = useState('');
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (!open) return;
const next = buildShareUrl();
setUrl(next);
setCopied(false);
track('share_popover_opened');
// Auto-select the URL so Cmd/Ctrl+C just works.
queueMicrotask(() => {
inputRef.current?.focus();
inputRef.current?.select();
});
}, [open]);

const handleCopy = useCallback(async () => {
const url = buildShareUrl();
const target = url || buildShareUrl();
track('share_link_copied');

try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
await navigator.clipboard.writeText(target);
} catch {
// fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = url;
textArea.value = target;
document.body.append(textArea);
textArea.select();
document.execCommand('copy');
textArea.remove();
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}

// Dispatch action event for post-action star prompt
setCopied(true);
setTimeout(() => setCopied(false), 2000);
window.dispatchEvent(new CustomEvent('inferencex:action'));
}, []);
}, [url]);

return (
<Button
data-testid="share-button"
variant="outline"
size="sm"
onClick={handleCopy}
className="h-7 gap-1.5 text-xs"
title="Copy share link to clipboard"
>
{copied ? (
<>
<Check className="size-3" />
Copied
</>
) : (
<>
<LinkIcon className="size-3" />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
data-testid="share-button"
size="sm"
className="h-8 gap-1.5 text-xs font-medium"
title="Share this view"
>
<Share2 className="size-3.5" />
Share
</>
)}
</Button>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="w-80"
data-testid="share-popover"
onOpenAutoFocus={(event) => {
// Keep focus on the URL input rather than the first focusable child.
event.preventDefault();
inputRef.current?.focus();
inputRef.current?.select();
}}
>
<div className="space-y-3">
<div>
<h4 className="text-sm font-semibold">Share this view</h4>
<p className="text-muted-foreground text-xs mt-0.5">
Anyone with this link will see your current selections and filters.
</p>
</div>
<div className="flex items-center gap-1.5">
<input
ref={inputRef}
data-testid="share-url-input"
readOnly
value={url}
onFocus={(event) => event.currentTarget.select()}
className="border-input bg-background h-8 flex-1 min-w-0 rounded-md border px-2 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
<Button
data-testid="share-copy-button"
size="sm"
variant="outline"
className="h-8 gap-1.5 text-xs shrink-0"
onClick={handleCopy}
>
{copied ? (
<>
<Check className="size-3.5" />
Copied
</>
) : (
<>
<Copy className="size-3.5" />
Copy
</>
)}
</Button>
</div>
<div className="flex items-center justify-between border-t pt-3">
<span className="text-muted-foreground text-xs">Or share on</span>
<div className="flex items-center gap-1.5">
<ShareTwitterButton />
<ShareLinkedInButton />
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
}
2 changes: 1 addition & 1 deletion packages/app/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from 'path';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
coverage: {
provider: 'v8',
include: ['src/lib/**/*.ts', 'src/scripts/**/*.ts', 'src/app/api/**/*.ts'],
Expand Down
Loading