1
0

share-link-inspector.tsx 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. 'use client';
  2. import { useMemo, useState } from 'react';
  3. import { AlertCircle } from 'lucide-react';
  4. import { parseLink, type ParsedLink } from '@/lib/xray/links';
  5. import { ToolFrame } from './tool-frame';
  6. type Result = { ok: true; data: ParsedLink } | { ok: false; error: string } | null;
  7. export function ShareLinkInspector() {
  8. const [input, setInput] = useState('');
  9. const result: Result = useMemo(() => {
  10. const value = input.trim();
  11. if (!value) return null;
  12. try {
  13. return { ok: true, data: parseLink(value) };
  14. } catch (e) {
  15. return { ok: false, error: (e as Error).message };
  16. }
  17. }, [input]);
  18. return (
  19. <ToolFrame
  20. title="Share-link inspector"
  21. description="Paste a vless / vmess / trojan / ss link to decode every parameter. It is parsed entirely in your browser — nothing is sent over the network."
  22. onReset={input ? () => setInput('') : undefined}
  23. >
  24. <textarea
  25. value={input}
  26. onChange={(e) => setInput(e.target.value)}
  27. placeholder="vless://uuid@host:443?security=reality&pbk=...#name"
  28. dir="ltr"
  29. rows={3}
  30. spellCheck={false}
  31. className="w-full resize-y rounded-lg border bg-fd-background px-3 py-2 font-mono text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30"
  32. />
  33. {result && !result.ok ? (
  34. <div className="mt-3 flex items-center gap-2 rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">
  35. <AlertCircle className="size-4 shrink-0" aria-hidden />
  36. <span>{result.error}</span>
  37. </div>
  38. ) : null}
  39. {result && result.ok ? <ResultTable data={result.data} /> : null}
  40. </ToolFrame>
  41. );
  42. }
  43. function ResultTable({ data }: { data: ParsedLink }) {
  44. const rows: [string, string][] = [
  45. ['Protocol', data.protocol],
  46. ['Name', data.name],
  47. ['Address', data.address],
  48. ['Port', String(data.port)],
  49. [data.protocol === 'trojan' ? 'Password' : 'ID / credential', data.credential],
  50. ...Object.entries(data.params),
  51. ];
  52. return (
  53. <div className="mt-3 overflow-hidden rounded-xl border">
  54. <table className="w-full text-sm">
  55. <tbody>
  56. {rows.map(([key, value], i) => (
  57. <tr key={`${key}-${i}`} className="border-b last:border-b-0">
  58. <th
  59. scope="row"
  60. className="w-1/3 bg-fd-muted/40 px-3 py-2 text-start align-top font-medium text-fd-muted-foreground"
  61. >
  62. {key}
  63. </th>
  64. <td dir="ltr" className="break-all px-3 py-2 text-start font-mono">
  65. {value || <span className="text-fd-muted-foreground">—</span>}
  66. </td>
  67. </tr>
  68. ))}
  69. </tbody>
  70. </table>
  71. </div>
  72. );
  73. }