mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
Redo setup page onboarding UI (#1659)
## Summary
- Reworks the setup page into recommended/manual onboarding flows with
clearer setup guidance and an explicit MCP verification step.
- Polishes setup UI surfaces, framework selector cards, step indicators,
code blocks, and key display styling.
- Replaces raw env key textareas with a masked env-file viewer and
reveal/copy controls.
## Screenshots
### Recommended Setup
| | Light | Dark |
| --- | --- | --- |
| Overview | 
| 
|
| Keys | 
| 
|
### Manual Setup
Framework selector:

Next.js keys:

Code block syntax highlighting:

TanStack Start keys:

## Test plan
- [x] Ran `pnpm --filter @hexclave/dashboard lint`
- [x] Manually checked recommended setup, manual setup, masked/revealed
keys, and light/dark mode screenshots
Made with [Cursor](https://cursor.com)
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Redesigned the Setup page around a single cloud‑first prompt and a
docs‑linked Manual path, with a masked `.env` viewer and refreshed code
panels. Environment‑aware docs/API URLs, explicit MCP verification, and
stricter tab handling make onboarding clearer and safer.
- **New Features**
- Recommended flow shows a prefilled cloud setup prompt (API URL and
project ID) inline; Manual opens the latest setup docs.
- Replaced textareas with `EnvFileViewer` for `.env.local`/`.env`
(reveal-all and copy file); “Generate Keys” CTA when keys are missing.
- Secret inputs in `CopyField` support show/hide; `CodeBlock` uses
shared `codePanelShellClasses` and header.
- Added a final “Done” step with an Explore Dashboard CTA.
- **Bug Fixes**
- MCP server registration details are correct and explicitly called out
for verification (name `hexclave`, URL `https://mcp.hexclave.com/mcp`).
- Tabs fail fast on unexpected values; docs/API base URLs read from
`NEXT_PUBLIC_STACK_DOCS_BASE_URL`/`NEXT_PUBLIC_STACK_API_URL` with
fallbacks; updated `.env.development` docs URL.
<sup>Written for commit 2cd3486ac6.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1659?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Setup onboarding now uses “Recommended” and “Manual setup” tabs with
an ordered checklist and a shared in-page setup prompt.
* Environment key generation now uses a line-by-line env file viewer
with masking/reveal and copy support, including “Generate Keys” when
keys are missing.
* Secret fields now include show/hide toggles.
* **Bug Fixes**
* Documentation links now open the correct constructed URLs based on the
configured docs base URL (with a sensible fallback).
* **Style**
* Refreshed code panel and input/textarea theming, including improved
light/dark styling and updated copy-field behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
d2a84f5a28
commit
2474b600de
@ -1,5 +1,5 @@
|
||||
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
|
||||
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26
|
||||
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04
|
||||
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
|
||||
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false
|
||||
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
import { CodeBlock, codePanelShellClasses } from '@/components/code-block';
|
||||
import { DesignButton } from "@/components/design-components";
|
||||
import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys } from '@/components/env-keys';
|
||||
import { EnvFileViewer } from '@/components/env-keys';
|
||||
import { InlineCode } from '@/components/inline-code';
|
||||
import { StyledLink } from '@/components/link';
|
||||
import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
|
||||
import { Tabs, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
|
||||
import { getPublicEnvVar } from '@/lib/env';
|
||||
import { useThemeWatcher } from '@/lib/theme';
|
||||
import { BookIcon, SparkleIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { BookIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { remindersPrompt } from '@hexclave/shared/dist/ai/unified-prompts/reminders';
|
||||
import { use } from "@hexclave/shared/dist/utils/react";
|
||||
import { deindent } from '@hexclave/shared/dist/utils/strings';
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from 'next/image';
|
||||
import { Suspense, useRef, useState } from "react";
|
||||
import type { GlobeMethods } from 'react-globe.gl';
|
||||
import { PageLayout } from "../page-layout";
|
||||
@ -22,71 +22,63 @@ import styles from './setup-page.module.css';
|
||||
const countriesPromise = import('./country-data.geo.json');
|
||||
const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default), { ssr: false });
|
||||
|
||||
const commandClasses = "text-red-600 dark:text-red-400";
|
||||
const nameClasses = "text-green-600 dark:text-green-500";
|
||||
type SetupMode = "recommended" | "manual";
|
||||
|
||||
const INSTALL_COMMAND_BY_FRAMEWORK = {
|
||||
nextjs: 'npx @hexclave/cli@latest init',
|
||||
tanstackStart: 'npm install @hexclave/tanstack-start',
|
||||
react: 'npm install @hexclave/react',
|
||||
javascript: 'npm install @hexclave/js',
|
||||
python: 'pip install requests',
|
||||
} as const;
|
||||
const PROD_DOCS_BASE_URL = 'https://docs.hexclave.com';
|
||||
const PROD_API_BASE_URL = 'https://api.hexclave.com';
|
||||
|
||||
type SetupFramework = keyof typeof INSTALL_COMMAND_BY_FRAMEWORK;
|
||||
function getSetupDocsBaseUrl() {
|
||||
return getPublicEnvVar('NEXT_PUBLIC_STACK_DOCS_BASE_URL') ?? PROD_DOCS_BASE_URL;
|
||||
}
|
||||
|
||||
const TANSTACK_START_SETUP_PROMPT = deindent`
|
||||
Please set up Hexclave in my TanStack Start app.
|
||||
function getManualSetupDocsUrl() {
|
||||
const docsBaseUrl = getSetupDocsBaseUrl().replace(/\/$/, '');
|
||||
return `${docsBaseUrl}/guides/getting-started/setup`;
|
||||
}
|
||||
|
||||
1. Install the alpha TanStack Start package:
|
||||
function getSetupApiBaseUrl() {
|
||||
return getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') ?? PROD_API_BASE_URL;
|
||||
}
|
||||
|
||||
npm install @hexclave/tanstack-start
|
||||
function buildCloudSetupPrompt(options: {
|
||||
docsBaseUrl: string,
|
||||
projectId: string,
|
||||
apiBaseUrl: string,
|
||||
}) {
|
||||
const { docsBaseUrl, projectId, apiBaseUrl } = options;
|
||||
const normalizedDocsBaseUrl = docsBaseUrl.replace(/\/$/, '');
|
||||
const reminders = remindersPrompt.replaceAll(PROD_DOCS_BASE_URL, normalizedDocsBaseUrl);
|
||||
|
||||
2. Configure the app with these environment variables:
|
||||
|
||||
VITE_HEXCLAVE_PROJECT_ID=<project-id>
|
||||
HEXCLAVE_SECRET_SERVER_KEY=<secret-server-key>
|
||||
|
||||
3. Create a HexclaveClientApp using @hexclave/tanstack-start with:
|
||||
- projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID
|
||||
- tokenStore: "cookie"
|
||||
- redirectMethod: "window"
|
||||
|
||||
4. Wrap the TanStack Start root route with HexclaveProvider and HexclaveTheme.
|
||||
|
||||
5. Add a /handler/$ route using HexclaveHandler. The handler route must set ssr: false and pass location={pathname} from useLocation().
|
||||
|
||||
Use only the environment variables listed above.
|
||||
|
||||
After it finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/\`. If it is not registered, please add it manually so you have live access to Hexclave docs and APIs.
|
||||
`;
|
||||
|
||||
const buildInstallPrompt = (framework: SetupFramework) => {
|
||||
if (framework === "tanstackStart") {
|
||||
return TANSTACK_START_SETUP_PROMPT;
|
||||
}
|
||||
|
||||
const command = INSTALL_COMMAND_BY_FRAMEWORK[framework];
|
||||
return deindent`
|
||||
Please run the following command in my project's terminal:
|
||||
Install and set up Hexclave in this project by following these instructions:
|
||||
|
||||
${command}
|
||||
Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language.
|
||||
|
||||
After it finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Hexclave docs and APIs.
|
||||
`;
|
||||
};
|
||||
Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup.
|
||||
|
||||
Do not change the dev script in package.json. In cloud setup, there's no need for that.
|
||||
|
||||
Use these Hexclave project values when creating environment variables:
|
||||
|
||||
- Hexclave API URL: ${apiBaseUrl}
|
||||
- Hexclave project ID: ${projectId}
|
||||
|
||||
Create the framework-specific public environment variables for the Hexclave API URL and project ID. For example, Next.js uses NEXT_PUBLIC_HEXCLAVE_API_URL and NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, while Vite-based frameworks use VITE_HEXCLAVE_API_URL and VITE_HEXCLAVE_PROJECT_ID. If the Hexclave docs for this framework specify different environment variable names, use the docs' framework-specific names with the values above.
|
||||
|
||||
After setup finishes, verify that the Hexclave MCP server is registered in your AI client config — name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs.
|
||||
|
||||
Once setup is done, tell me to add the Hexclave secret server key from the dashboard to my environment file. After that, setup is complete.
|
||||
|
||||
${reminders}
|
||||
`;
|
||||
}
|
||||
|
||||
export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
const adminApp = useAdminApp();
|
||||
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'>('nextjs');
|
||||
const [setupMode, setSetupMode] = useState<SetupMode>("recommended");
|
||||
const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null);
|
||||
const projectConfig = adminApp.useProject().useConfig();
|
||||
const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey;
|
||||
const publishableClientKeyValue = keys?.publishableClientKey ?? "...";
|
||||
const optionalPublishableClientKeyProp = (indent: string) =>
|
||||
requirePublishableClientKey ? `\n${indent}publishableClientKey: "${publishableClientKeyValue}",` : "";
|
||||
const optionalPublishableClientKeyHeader = (indent: string) =>
|
||||
requirePublishableClientKey ? `\n${indent}'x-hexclave-publishable-client-key': "${publishableClientKeyValue}",` : "";
|
||||
|
||||
const onGenerateKeys = async () => {
|
||||
const newKey = await adminApp.createInternalApiKey({
|
||||
@ -104,512 +96,12 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
});
|
||||
};
|
||||
|
||||
const nextJsSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing Next.js project, install Hexclave as a dependency into your project:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npx @hexclave/cli@latest init`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@hexclave/cli@latest</span> init
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Put these keys in the <InlineCode>.env.local</InlineCode> file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Done",
|
||||
content: <>
|
||||
<Typography>
|
||||
If you start your Next.js app with npm run dev and navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
},
|
||||
];
|
||||
|
||||
const reactSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing React project, install Hexclave's dependencies:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/react`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/react</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create hexclave/client.ts file",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file called <InlineCode>hexclave/client.ts</InlineCode> and add the following code. Here we use react-router-dom as an example.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const hexclaveClientApp = new HexclaveClientApp({
|
||||
// You should store these in environment variables
|
||||
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: {
|
||||
useNavigate,
|
||||
}
|
||||
});
|
||||
`}
|
||||
title="hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Update App.tsx",
|
||||
content: <>
|
||||
<Typography>
|
||||
Update your App.tsx file to wrap the entire app with a <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode> and add a <InlineCode>HexclaveHandler</InlineCode> component to handle the authentication flow.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
maxHeight={300}
|
||||
content={deindent`
|
||||
import { HexclaveHandler, HexclaveProvider, HexclaveTheme } from "@hexclave/react";
|
||||
import { Suspense } from "react";
|
||||
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { hexclaveClientApp } from "./hexclave/client";
|
||||
|
||||
function HandlerRoutes() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<HexclaveHandler app={hexclaveClientApp} location={location.pathname} fullPage />
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<BrowserRouter>
|
||||
<HexclaveProvider app={hexclaveClientApp}>
|
||||
<HexclaveTheme>
|
||||
<Routes>
|
||||
<Route path="/handler/*" element={<HandlerRoutes />} />
|
||||
<Route path="/" element={<div>hello world</div>} />
|
||||
</Routes>
|
||||
</HexclaveTheme>
|
||||
</HexclaveProvider>
|
||||
</BrowserRouter>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
`}
|
||||
title="App.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: "Done",
|
||||
content: <>
|
||||
<Typography>
|
||||
If you start your React app with npm run dev and navigate to <StyledLink href="http://localhost:5173/handler/signup">http://localhost:5173/handler/signup</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const tanstackStartSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
In a new or existing TanStack Start project, install the alpha Hexclave package:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/tanstack-start`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/tanstack-start</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Put these keys in your TanStack Start environment file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create hexclave/client.ts file",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file called <InlineCode>src/hexclave/client.ts</InlineCode> and initialize Hexclave with cookie storage.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/tanstack-start";
|
||||
|
||||
export const hexclaveClientApp = new HexclaveClientApp({
|
||||
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID,
|
||||
tokenStore: "cookie",
|
||||
redirectMethod: "window",
|
||||
});
|
||||
`}
|
||||
title="src/hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Update the root route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Wrap your TanStack Start root route with <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode>.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
maxHeight={300}
|
||||
content={deindent`
|
||||
import { HexclaveProvider, HexclaveTheme } from "@hexclave/tanstack-start";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import { hexclaveClientApp } from "../hexclave/client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
shellComponent: RootDocument,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<HexclaveProvider app={hexclaveClientApp}>
|
||||
<HexclaveTheme>
|
||||
<Outlet />
|
||||
</HexclaveTheme>
|
||||
</HexclaveProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
`}
|
||||
title="src/routes/__root.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: "Add the handler route",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a splat route for Hexclave's built-in auth pages.
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="tsx"
|
||||
content={deindent`
|
||||
import { HexclaveHandler } from "@hexclave/tanstack-start";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <HexclaveHandler fullPage location={pathname} />;
|
||||
}
|
||||
`}
|
||||
title="src/routes/handler/$.tsx"
|
||||
icon="code"
|
||||
/>
|
||||
<Typography>
|
||||
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
|
||||
</Typography>
|
||||
</>
|
||||
},
|
||||
];
|
||||
|
||||
const javascriptSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install Hexclave",
|
||||
content: <>
|
||||
<Typography>
|
||||
Install Hexclave using npm:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`npm install @hexclave/js`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/js</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Initialize the app",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a new file for your Hexclave app initialization:
|
||||
</Typography>
|
||||
<Tabs defaultValue="server">
|
||||
<TabsList>
|
||||
<TabsTrigger value="server">Server</TabsTrigger>
|
||||
<TabsTrigger value="client">Client</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="server">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { HexclaveServerApp } from "@hexclave/js";
|
||||
|
||||
const hexclaveServerApp = new HexclaveServerApp({
|
||||
// You should store these in environment variables based on your project setup
|
||||
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
|
||||
secretServerKey: "${keys?.secretServerKey ?? "..."}",
|
||||
tokenStore: "memory",
|
||||
});
|
||||
`}
|
||||
title="hexclave/server.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="client">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { HexclaveClientApp } from "@hexclave/js";
|
||||
|
||||
const hexclaveClientApp = new HexclaveClientApp({
|
||||
// You should store these in environment variables
|
||||
projectId: "your-project-id",${optionalPublishableClientKeyProp(" ")}
|
||||
tokenStore: "cookie",
|
||||
});
|
||||
`}
|
||||
title="hexclave/client.ts"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Example usage",
|
||||
content: <>
|
||||
<Tabs defaultValue="server">
|
||||
<TabsList>
|
||||
<TabsTrigger value="server">Server</TabsTrigger>
|
||||
<TabsTrigger value="client">Client</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="server">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { hexclaveServerApp } from "@/hexclave/server";
|
||||
|
||||
const user = await hexclaveServerApp.getUser("user_id");
|
||||
|
||||
await user.update({
|
||||
displayName: "New Display Name",
|
||||
});
|
||||
|
||||
const team = await hexclaveServerApp.createTeam({
|
||||
name: "New Team",
|
||||
});
|
||||
|
||||
await team.addUser(user.id);
|
||||
`}
|
||||
title="Example server usage"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="client">
|
||||
<CodeBlock
|
||||
language="typescript"
|
||||
content={deindent`
|
||||
import { hexclaveClientApp } from "@/hexclave/client";
|
||||
|
||||
await hexclaveClientApp.signInWithCredential({
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const user = await hexclaveClientApp.getUser();
|
||||
|
||||
await user.update({
|
||||
displayName: "New Display Name",
|
||||
});
|
||||
|
||||
await user.signOut();
|
||||
`}
|
||||
title="Example client usage"
|
||||
icon="code"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const pythonSteps = [
|
||||
{
|
||||
step: 2,
|
||||
title: "Install requests",
|
||||
content: <>
|
||||
<Typography>
|
||||
Install the requests library to make HTTP requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="bash"
|
||||
content={`pip install requests`}
|
||||
customRender={
|
||||
<div className="p-4 font-mono text-sm">
|
||||
<span className={commandClasses}>pip install</span> <span className={nameClasses}>requests</span>
|
||||
</div>
|
||||
}
|
||||
title="Terminal"
|
||||
icon="terminal"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Create Keys",
|
||||
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Create helper function",
|
||||
content: <>
|
||||
<Typography>
|
||||
Create a helper function to make requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="python"
|
||||
content={deindent`
|
||||
import requests
|
||||
|
||||
def stack_auth_request(method, endpoint, **kwargs):
|
||||
res = requests.request(
|
||||
method,
|
||||
f'https://api.hexclave.com/{endpoint}',
|
||||
headers={
|
||||
'x-hexclave-access-type': 'server',
|
||||
# You should store these in environment variables
|
||||
'x-hexclave-project-id': "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyHeader(" ")}
|
||||
'x-hexclave-secret-server-key': "${keys?.secretServerKey ?? "..."}",
|
||||
**kwargs.pop('headers', {}),
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
if res.status_code >= 400:
|
||||
raise Exception(f"Hexclave API request failed with {res.status_code}: {res.text}")
|
||||
return res.json()
|
||||
`}
|
||||
title="stack_auth.py"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: "Make requests",
|
||||
content: <>
|
||||
<Typography>
|
||||
You can now make requests to the Hexclave API:
|
||||
</Typography>
|
||||
<CodeBlock
|
||||
language="python"
|
||||
content={deindent`
|
||||
# Get current project info
|
||||
print(stack_auth_request('GET', '/api/v1/projects/current'))
|
||||
|
||||
# Get user info with access token
|
||||
print(stack_auth_request('GET', '/api/v1/users/me', headers={
|
||||
'x-hexclave-access-token': access_token,
|
||||
}))
|
||||
`}
|
||||
title="example.py"
|
||||
icon="code"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
];
|
||||
|
||||
const selectedInstallPrompt = buildCloudSetupPrompt({
|
||||
docsBaseUrl: getSetupDocsBaseUrl(),
|
||||
projectId: adminApp.projectId,
|
||||
apiBaseUrl: getSetupApiBaseUrl(),
|
||||
});
|
||||
const manualSetupDocsUrl = getManualSetupDocsUrl();
|
||||
|
||||
return (
|
||||
<PageLayout width={1000}>
|
||||
@ -619,7 +111,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
<XIcon className="w-4 h-4 ml-1 mt-0.5" />
|
||||
</DesignButton>
|
||||
</div>
|
||||
<div className="flex gap-4 justify-center items-center border rounded-2xl py-4 px-8 backdrop-blur-md bg-slate-200/20 dark:bg-black/20">
|
||||
<div className="flex gap-4 justify-center items-center rounded-2xl py-4 px-8 backdrop-blur-md bg-white/60 dark:bg-background/40 ring-1 ring-black/[0.06] dark:ring-white/[0.06] border border-black/[0.06] dark:border-white/[0.06] shadow-sm">
|
||||
<GlobeIllustration />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
@ -638,7 +130,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
window.open('https://docs.hexclave.com/', '_blank');
|
||||
window.open(getSetupDocsBaseUrl(), '_blank');
|
||||
}}
|
||||
>
|
||||
<BookIcon className="w-4 h-4 mr-2" />
|
||||
@ -649,91 +141,81 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-8 mx-4">
|
||||
<CopyPromptButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
content={buildInstallPrompt(selectedFramework)}
|
||||
>
|
||||
<SparkleIcon className="w-4 h-4 mr-2 text-purple-500 dark:text-purple-400" weight="fill" />
|
||||
Copy prompt
|
||||
</CopyPromptButton>
|
||||
<Tabs value={setupMode} onValueChange={(value) => setSetupMode(value === "manual" ? "manual" : "recommended")}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="recommended">Recommended</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual setup</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-4 mx-4">
|
||||
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
title: "Select your framework",
|
||||
content: <div>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{([{
|
||||
id: 'nextjs',
|
||||
name: 'Next.js',
|
||||
reverseIfDark: true,
|
||||
imgSrc: '/next-logo.svg',
|
||||
}, {
|
||||
id: 'tanstackStart',
|
||||
name: 'TanStack Start',
|
||||
reverseIfDark: false,
|
||||
imgSrc: '/tanstack-start-logo.png',
|
||||
}, {
|
||||
id: 'react',
|
||||
name: 'React',
|
||||
reverseIfDark: false,
|
||||
imgSrc: '/react-logo.svg',
|
||||
}, {
|
||||
id: 'javascript',
|
||||
name: 'JavaScript',
|
||||
reverseIfDark: false,
|
||||
imgSrc: '/javascript-logo.svg',
|
||||
}, {
|
||||
id: 'python',
|
||||
name: 'Python',
|
||||
reverseIfDark: false,
|
||||
imgSrc: '/python-logo.svg',
|
||||
}] as const).map(({ name, imgSrc: src, reverseIfDark, id }) => (
|
||||
<DesignButton
|
||||
key={id}
|
||||
variant={id === selectedFramework ? 'secondary' : 'plain'} className='h-24 w-24 flex flex-col items-center justify-center gap-2 '
|
||||
onClick={() => setSelectedFramework(id)}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={name}
|
||||
|
||||
className={reverseIfDark ? "dark:invert" : undefined}
|
||||
width="0"
|
||||
height="0"
|
||||
sizes="100vw"
|
||||
style={{ width: '30px', height: 'auto' }}
|
||||
/>
|
||||
<Typography type='label'>{name}</Typography>
|
||||
</DesignButton>
|
||||
))}
|
||||
{setupMode === "recommended" ? (
|
||||
<div className="flex flex-col mt-4 mx-4">
|
||||
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
title: "Copy Setup Prompt",
|
||||
content: <div className="flex min-w-0 flex-col gap-4">
|
||||
<CodeBlock
|
||||
language="text"
|
||||
content={selectedInstallPrompt}
|
||||
customRender={
|
||||
<pre className="max-h-[480px] overflow-y-auto whitespace-pre-wrap break-words p-4 text-sm leading-6 text-foreground">
|
||||
{selectedInstallPrompt}
|
||||
</pre>
|
||||
}
|
||||
title="Prompt for your AI agent"
|
||||
icon="code"
|
||||
maxHeight={480}
|
||||
/>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Create Keys",
|
||||
content: <>
|
||||
<Typography>
|
||||
Add this server-only key to your project's <InlineCode>.env.local</InlineCode> file.
|
||||
</Typography>
|
||||
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} />
|
||||
</>,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Done",
|
||||
content: <SetupRecommendedDoneStep onExploreDashboard={props.toMetrics} />,
|
||||
},
|
||||
].map((item) => (
|
||||
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
|
||||
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
|
||||
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
|
||||
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
|
||||
</span>
|
||||
<h3 className="font-medium leading-tight">{item.title}</h3>
|
||||
</div>
|
||||
</div>,
|
||||
},
|
||||
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
|
||||
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
|
||||
...(selectedFramework === 'react' ? reactSteps : []),
|
||||
...(selectedFramework === 'javascript' ? javascriptSteps : []),
|
||||
...(selectedFramework === 'python' ? pythonSteps : []),
|
||||
].map((item, index) => (
|
||||
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
|
||||
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
|
||||
<span className={`absolute flex items-center justify-center w-8 h-8 bg-gray-100 dark:bg-gray-70 rounded-full -start-4 ring-4 ring-white dark:ring-gray-900`}>
|
||||
<span className={`text-gray-500 dark:text-gray-700 font-medium`}>{item.step}</span>
|
||||
</span>
|
||||
<h3 className="font-medium leading-tight">{item.title}</h3>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
{item.content}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-grow flex-col gap-4">
|
||||
{item.content}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-4 mt-12 flex flex-col items-center gap-4 py-16 text-center">
|
||||
<Typography>
|
||||
Manual setup steps live in the documentation so they stay up to date with every framework and SDK change.
|
||||
</Typography>
|
||||
<DesignButton
|
||||
onClick={() => {
|
||||
window.open(manualSetupDocsUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<BookIcon className="w-4 h-4 mr-2" />
|
||||
Open manual setup docs
|
||||
</DesignButton>
|
||||
</div>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@ -813,45 +295,44 @@ function GlobeIllustrationInner() {
|
||||
);
|
||||
}
|
||||
|
||||
function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography>
|
||||
Hooray! Setup completed.
|
||||
</Typography>
|
||||
<div>
|
||||
<DesignButton onClick={props.onExploreDashboard}>
|
||||
Explore Dashboard
|
||||
</DesignButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HexclaveKeys(props: {
|
||||
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
|
||||
onGenerateKeys: () => Promise<void>,
|
||||
type: 'next' | 'vite' | 'raw',
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full border rounded-xl p-8 gap-4 flex flex-col">
|
||||
{props.keys ? (
|
||||
<>
|
||||
{props.type === 'next' ? (
|
||||
<NextJsEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
publishableClientKey={props.keys.publishableClientKey}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : props.type === 'vite' ? (
|
||||
<ViteEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
) : (
|
||||
<APIEnvKeys
|
||||
projectId={props.keys.projectId}
|
||||
publishableClientKey={props.keys.publishableClientKey}
|
||||
secretServerKey={props.keys.secretServerKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Typography type="label" variant="secondary">
|
||||
{`Save these keys securely - they won't be shown again after leaving this page.`}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
if (!props.keys) {
|
||||
return (
|
||||
<div className={cn(codePanelShellClasses, "w-full p-5 flex flex-col")}>
|
||||
<div className="flex items-center justify-center">
|
||||
<DesignButton onClick={props.onGenerateKeys}>
|
||||
Generate Keys
|
||||
</DesignButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<EnvFileViewer filename=".env.local" value={`HEXCLAVE_SECRET_SERVER_KEY=${props.keys.secretServerKey}`} />
|
||||
|
||||
<Typography type="label" variant="secondary">
|
||||
{`Save these keys securely - they won't be shown again after leaving this page.`}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@ Object.entries({ tsx, bash, typescript, python, sql }).forEach(([key, value]) =>
|
||||
SyntaxHighlighter.registerLanguage(key, value);
|
||||
});
|
||||
|
||||
export const codePanelShellClasses = "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]";
|
||||
|
||||
export const codePanelHeaderClasses = "text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015] py-2.5 border-b border-black/[0.06] dark:border-white/[0.06]";
|
||||
|
||||
type CodeBlockProps = {
|
||||
language: string,
|
||||
content: string,
|
||||
@ -32,7 +36,7 @@ type CodeBlockProps = {
|
||||
};
|
||||
|
||||
export function CodeBlock(props: CodeBlockProps) {
|
||||
const { theme, mounted } = useThemeWatcher();
|
||||
const { theme } = useThemeWatcher();
|
||||
|
||||
let icon = null;
|
||||
switch (props.icon) {
|
||||
@ -47,8 +51,20 @@ export function CodeBlock(props: CodeBlockProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("overflow-hidden", !props.fullWidth && "rounded-xl", props.neutralBackground ? "bg-background" : "bg-muted")}>
|
||||
<div className={cn("text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center", props.compact && !props.noSeparator && "py-1", !props.compact && !props.noSeparator && "py-2", props.noSeparator && "pt-1 pb-0", !props.noSeparator && "border-b")}>
|
||||
<div className={cn(
|
||||
props.neutralBackground
|
||||
? "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
|
||||
: !props.fullWidth
|
||||
? codePanelShellClasses
|
||||
: "overflow-hidden transition-all duration-150 hover:transition-none bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]"
|
||||
)}>
|
||||
<div className={cn(
|
||||
"text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015]",
|
||||
props.compact && !props.noSeparator && "py-1.5",
|
||||
!props.compact && !props.noSeparator && "py-2.5",
|
||||
props.noSeparator && "pt-1 pb-0",
|
||||
!props.noSeparator && "border-b border-black/[0.06] dark:border-white/[0.06]"
|
||||
)}>
|
||||
<h5 className={cn("font-medium flex items-center gap-2", props.compact && "text-xs")}>
|
||||
{icon}
|
||||
{props.title}
|
||||
|
||||
@ -1,7 +1,86 @@
|
||||
import { getPublicEnvVar } from '@/lib/env';
|
||||
import { Button, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
|
||||
"use client";
|
||||
|
||||
function getEnvFileContent(props: {
|
||||
import { codePanelHeaderClasses, codePanelShellClasses } from '@/components/code-block';
|
||||
import { getPublicEnvVar } from '@/lib/env';
|
||||
import { Button, CopyButton, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
|
||||
import React, { useState } from "react";
|
||||
import { EyeIcon, EyeSlashIcon, FileTextIcon } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type EnvFileViewerProps = {
|
||||
filename: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
|
||||
const [revealAll, setRevealAll] = useState(false);
|
||||
|
||||
const lines = value.split("\n").map((line, idx) => {
|
||||
const eqIndex = line.indexOf("=");
|
||||
if (eqIndex === -1) return { key: `comment_${idx}`, val: line, isComment: true };
|
||||
const key = line.substring(0, eqIndex);
|
||||
const val = line.substring(eqIndex + 1);
|
||||
return { key, val, isComment: false };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn(codePanelShellClasses, "w-full flex flex-col")}>
|
||||
<div className={codePanelHeaderClasses}>
|
||||
<h5 className="font-medium flex items-center gap-2">
|
||||
<FileTextIcon className="w-4 h-4" />
|
||||
{filename}
|
||||
</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-6 w-6 p-1"
|
||||
onClick={() => setRevealAll(!revealAll)}
|
||||
title={revealAll ? "Mask values" : "Reveal values"}
|
||||
aria-label={revealAll ? "Mask values" : "Reveal values"}
|
||||
>
|
||||
{revealAll ? <EyeSlashIcon className="h-3.5 w-3.5" /> : <EyeIcon className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
<CopyButton content={value} variant="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto p-4 font-mono text-xs select-text">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
{lines.map((line, idx) => {
|
||||
return (
|
||||
<tr key={idx} className="group hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors leading-relaxed">
|
||||
<td className="py-0.5 w-full">
|
||||
<div className="flex items-center justify-between gap-4 w-full">
|
||||
<div className="flex items-center flex-wrap whitespace-pre">
|
||||
{line.isComment ? (
|
||||
<span className="text-muted-foreground/50 italic">{line.val}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-indigo-600 dark:text-indigo-400 font-medium select-all">{line.key}</span>
|
||||
<span className="text-muted-foreground/50 mx-1">=</span>
|
||||
{revealAll ? (
|
||||
<span className="text-teal-600 dark:text-teal-400 font-medium break-all select-all">{line.val}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/45 tracking-[0.25em] font-sans text-xs select-none">••••••••••••••••••••</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function getEnvFileContent(props: {
|
||||
projectId: string,
|
||||
publishableClientKey?: string,
|
||||
secretServerKey?: string,
|
||||
@ -91,6 +170,7 @@ export function APIEnvKeys(props: {
|
||||
<CopyField
|
||||
type="input"
|
||||
monospace
|
||||
isSecret
|
||||
value={props.secretServerKey}
|
||||
label="Secret Server Key"
|
||||
helper="This key is used on the server-side and can be used to perform actions on behalf of your users. Keep it safe."
|
||||
@ -100,6 +180,7 @@ export function APIEnvKeys(props: {
|
||||
<CopyField
|
||||
type="input"
|
||||
monospace
|
||||
isSecret
|
||||
value={props.superSecretAdminKey}
|
||||
label="Super Secret Admin Key"
|
||||
helper="This key is for administrative use only. Anyone owning this key will be able to create unlimited new keys and revoke any other keys. Be careful!"
|
||||
@ -118,13 +199,7 @@ export function NextJsEnvKeys(props: {
|
||||
const envFileContent = getEnvFileContent(props);
|
||||
|
||||
return (
|
||||
<CopyField
|
||||
type="textarea"
|
||||
monospace
|
||||
height={envFileContent.split("\n").length * 26}
|
||||
value={envFileContent}
|
||||
fixedSize
|
||||
/>
|
||||
<EnvFileViewer filename=".env.local" value={envFileContent} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,12 +217,6 @@ export function ViteEnvKeys(props: {
|
||||
.join("\n");
|
||||
|
||||
return (
|
||||
<CopyField
|
||||
type="textarea"
|
||||
monospace
|
||||
height={envFileContent.split("\n").length * 26}
|
||||
value={envFileContent}
|
||||
fixedSize
|
||||
/>
|
||||
<EnvFileViewer filename=".env" value={envFileContent} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Input } from "./input";
|
||||
import { Label } from "./label";
|
||||
import { SimpleTooltip } from "./simple-tooltip";
|
||||
import { Textarea } from "./textarea";
|
||||
import { CopyButton } from "./copy-button";
|
||||
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||
|
||||
export function CopyField(props: {
|
||||
value: string,
|
||||
@ -11,12 +15,15 @@ export function CopyField(props: {
|
||||
monospace?: boolean,
|
||||
fixedSize?: boolean,
|
||||
initialCopied?: boolean,
|
||||
isSecret?: boolean,
|
||||
} & ({
|
||||
type: "textarea",
|
||||
height?: number,
|
||||
} | {
|
||||
type: "input",
|
||||
})) {
|
||||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.label && (
|
||||
@ -43,12 +50,24 @@ export function CopyField(props: {
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
type={props.isSecret && !isRevealed ? "password" : "text"}
|
||||
value={props.value}
|
||||
className={props.isSecret ? "font-mono pr-10" : undefined}
|
||||
style={{
|
||||
fontFamily: props.monospace ? "ui-monospace, monospace" : "inherit",
|
||||
}}
|
||||
/>
|
||||
<CopyButton content={props.value} initialCopied={props.initialCopied} />
|
||||
{props.isSecret && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsRevealed(!isRevealed)}
|
||||
className="h-9 w-9 flex items-center justify-center rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm text-muted-foreground/60 hover:text-foreground hover:bg-white dark:hover:bg-foreground/[0.06] transition-all shrink-0"
|
||||
title={isRevealed ? "Hide key" : "Show key"}
|
||||
>
|
||||
{isRevealed ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
|
||||
</button>
|
||||
)}
|
||||
<CopyButton content={props.value} initialCopied={props.initialCopied} className="h-9 w-9 p-1.5 rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm hover:bg-white dark:hover:bg-foreground/[0.06] transition-all shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ const Textarea = forwardRefIfNeeded<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"stack-scope flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"stack-scope flex min-h-[60px] w-full rounded-xl border border-black/[0.08] dark:border-white/[0.06] bg-white/80 dark:bg-foreground/[0.03] shadow-sm ring-1 ring-black/[0.08] dark:ring-white/[0.06] placeholder:text-muted-foreground/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/[0.1] disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-150 hover:transition-none hover:bg-white dark:hover:bg-foreground/[0.06] px-3 py-2 text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user