blob: fcf68ce5f9858172dbc5019037960301d87a672e [file]
import {translate} from '@docusaurus/Translate';
import React, { isValidElement, JSX, useEffect, useRef, useState } from 'react';
import clsx from 'clsx';
import copy from 'copy-to-clipboard';
import { CodeLanguage, parseLineRanges, tokenizeCodeLine } from './hackathon.logic';
import { IconCopy } from './HackathonIcons';
export interface CodeSnippet {
/** Header label, e.g. "M1 ยท Keyword search with BM25". */
label: string;
/** Small file/language note next to the label, e.g. "sql". */
file: string;
language: CodeLanguage;
code: string;
/** Soft-wrap long lines (context prompts). */
wrap?: boolean;
/** Show only in the sticky panel, not inline under the step. */
panelOnly?: boolean;
/** When a panel view has several snippets, this one takes the step highlight. */
target?: boolean;
}
export function isCodeSnippet(value: unknown): value is CodeSnippet {
return (
typeof value === 'object' &&
value !== null &&
!isValidElement(value) &&
typeof (value as CodeSnippet).code === 'string' &&
typeof (value as CodeSnippet).language === 'string'
);
}
interface CopyButtonProps {
text: string;
className: string;
label?: string;
/** Accessible name when the visible label is hidden on small screens. */
ariaLabel?: string;
}
export function CopyButton({ text, className, label = translate({id: "landing.HackathonCodeBlock.001", message: "Copy"}), ariaLabel }: CopyButtonProps): JSX.Element {
const [copied, setCopied] = useState(false);
const timer = useRef<number | undefined>(undefined);
useEffect(() => () => window.clearTimeout(timer.current), []);
const onCopy = () => {
copy(text, { format: 'text/plain' });
setCopied(true);
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(false), 1600);
};
return (
<button type="button" className={clsx(className, copied && 'is-done')} onClick={onCopy} aria-label={ariaLabel}>
<IconCopy />
<span className="hk-task__copy-label" aria-live="polite">
{copied ? translate({id: "landing.HackathonCodeBlock.002", message: "Copied"}) : label}
</span>
</button>
);
}
interface HackathonCodeBlockProps {
snippet: CodeSnippet;
/** Line ranges to highlight, e.g. "4-6". The other lines are dimmed. */
highlight?: string;
pinnedLine?: number | null;
}
export function HackathonCodeBlock({ snippet, highlight, pinnedLine }: HackathonCodeBlockProps): JSX.Element {
const highlighted = new Set(parseLineRanges(highlight));
const lines = snippet.code.split('\n');
return (
<div className="hk-task__code">
<div className="hk-task__codehead">
<span className="hk-task__codehead-label">{snippet.label}</span>
<span className="hk-task__codehead-file">{snippet.file}</span>
<CopyButton className="hk-task__copy" text={snippet.code} />
</div>
<pre
className={clsx(
'hk-task__pre',
`hk-task__pre--${snippet.language}`,
snippet.wrap && 'hk-task__pre--wrap',
highlighted.size > 0 && 'is-dim',
)}
// Long lines scroll inside the block; keyboard users need a focus stop to scroll it.
tabIndex={0}
>
<code>
{lines.map((line, index) => {
const number = index + 1;
return (
<span
key={number}
className={clsx(
'hk-task__ln',
highlighted.has(number) && 'is-hl',
pinnedLine === number && 'is-pin',
)}
data-n={number}
>
{tokenizeCodeLine(line, snippet.language).map((token, tokenIndex) =>
token.kind ? (
<span key={tokenIndex} className={`hk-tok hk-tok--${token.kind}`}>
{token.text}
</span>
) : (
<React.Fragment key={tokenIndex}>{token.text}</React.Fragment>
),
)}
</span>
);
})}
</code>
</pre>
</div>
);
}