mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 23:56:07 +00:00
docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildSubscriptionUrls,
|
||||
buildShareLinks,
|
||||
buildBase64Subscription,
|
||||
buildJsonSubscription,
|
||||
type SubClient,
|
||||
type SubUrlInput,
|
||||
} from '@/lib/xray/subscription';
|
||||
import type { Network, Security } from '@/lib/xray/outbounds';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField, CheckboxField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
type ClientProtocol = 'vless' | 'vmess' | 'trojan' | 'ss';
|
||||
|
||||
interface ClientRow {
|
||||
protocol: ClientProtocol;
|
||||
remark: string;
|
||||
address: string;
|
||||
port: string;
|
||||
credential: string; // id (vless/vmess) or password (trojan/ss)
|
||||
method: string; // ss
|
||||
network: Network;
|
||||
security: Security;
|
||||
sni: string;
|
||||
}
|
||||
|
||||
const PROTOCOLS: readonly ClientProtocol[] = ['vless', 'vmess', 'trojan', 'ss'];
|
||||
const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
|
||||
const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
|
||||
|
||||
const addBtn =
|
||||
'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
|
||||
|
||||
const DEFAULT_CLIENTS: ClientRow[] = [
|
||||
{
|
||||
protocol: 'vless',
|
||||
remark: 'HK-01',
|
||||
address: 'a.example.com',
|
||||
port: '443',
|
||||
credential: '11111111-2222-3333-4444-555555555555',
|
||||
method: '',
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
sni: 'www.microsoft.com',
|
||||
},
|
||||
];
|
||||
|
||||
function toClient(r: ClientRow): SubClient {
|
||||
const isUuid = r.protocol === 'vless' || r.protocol === 'vmess';
|
||||
return {
|
||||
protocol: r.protocol,
|
||||
remark: r.remark,
|
||||
address: r.address,
|
||||
port: Number(r.port),
|
||||
id: isUuid ? r.credential : undefined,
|
||||
password: isUuid ? undefined : r.credential,
|
||||
method: r.protocol === 'ss' ? r.method : undefined,
|
||||
network: r.network,
|
||||
security: r.security,
|
||||
sni: r.sni || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function SubscriptionBuilder() {
|
||||
const [scheme, setScheme] = useState<'http' | 'https'>('https');
|
||||
const [host, setHost] = useState('sub.example.com');
|
||||
const [port, setPort] = useState('2096');
|
||||
const [subPath, setSubPath] = useState('/sub/');
|
||||
const [jsonPath, setJsonPath] = useState('/json/');
|
||||
const [subId, setSubId] = useState('user-1');
|
||||
const [behindProxy, setBehindProxy] = useState(false);
|
||||
const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
|
||||
|
||||
function patch(i: number, p: Partial<ClientRow>) {
|
||||
setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
|
||||
}
|
||||
|
||||
const urlInput: SubUrlInput = { scheme, host, port: Number(port), subPath, jsonPath, subId, behindProxy };
|
||||
const urls = buildSubscriptionUrls(urlInput);
|
||||
const subClients = clients.filter((c) => c.address.trim()).map(toClient);
|
||||
|
||||
function reset() {
|
||||
setScheme('https');
|
||||
setHost('sub.example.com');
|
||||
setPort('2096');
|
||||
setSubPath('/sub/');
|
||||
setJsonPath('/json/');
|
||||
setSubId('user-1');
|
||||
setBehindProxy(false);
|
||||
setClients(DEFAULT_CLIENTS);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Subscription & sub-JSON builder"
|
||||
description="Build the subscription URLs and preview both body formats — the Base64 link list and the JSON (Xray-json) config."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Scheme"
|
||||
value={scheme}
|
||||
onChange={(v) => setScheme(v as 'http' | 'https')}
|
||||
options={['https', 'http']}
|
||||
/>
|
||||
<TextField label="Host" value={host} onChange={setHost} />
|
||||
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
||||
<TextField label="Sub ID" value={subId} onChange={setSubId} />
|
||||
<TextField label="Sub path" value={subPath} onChange={setSubPath} />
|
||||
<TextField label="JSON path" value={jsonPath} onChange={setJsonPath} />
|
||||
<CheckboxField
|
||||
label="Behind a reverse proxy (omit the port)"
|
||||
checked={behindProxy}
|
||||
onChange={setBehindProxy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Base64 subscription URL" value={urls.base64} qr />
|
||||
<OutputBlock label="JSON subscription URL" value={urls.json} />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold">Clients in this subscription</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() =>
|
||||
setClients((p) => [
|
||||
...p,
|
||||
{
|
||||
protocol: 'vless',
|
||||
remark: '',
|
||||
address: '',
|
||||
port: '443',
|
||||
credential: '',
|
||||
method: '',
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
sni: '',
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Add client
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
{clients.map((c, i) => (
|
||||
<div key={i} className="rounded-xl border p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Protocol"
|
||||
value={c.protocol}
|
||||
onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
|
||||
options={PROTOCOLS}
|
||||
/>
|
||||
<TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
|
||||
<TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
|
||||
<TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
|
||||
<TextField
|
||||
label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
|
||||
value={c.credential}
|
||||
onChange={(v) => patch(i, { credential: v })}
|
||||
/>
|
||||
{c.protocol === 'ss' ? (
|
||||
<TextField label="Method" value={c.method} onChange={(v) => patch(i, { method: v })} />
|
||||
) : null}
|
||||
<SelectField
|
||||
label="Transport"
|
||||
value={c.network}
|
||||
onChange={(v) => patch(i, { network: v as Network })}
|
||||
options={NETWORKS}
|
||||
/>
|
||||
<SelectField
|
||||
label="Security"
|
||||
value={c.security}
|
||||
onChange={(v) => patch(i, { security: v as Security })}
|
||||
options={SECURITIES}
|
||||
/>
|
||||
{c.security !== 'none' ? (
|
||||
<TextField label="SNI" value={c.sni} onChange={(v) => patch(i, { sni: v })} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() => setClients((p) => p.filter((_, j) => j !== i))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Subscription links (decoded body)" value={buildShareLinks(subClients).join('\n')} />
|
||||
<OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
|
||||
<OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user