mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[Docs][Content] Github install, UI changes, platform selection (#1098)
## Summary
This PR improves the documentation for GitHub authentication setup and
self-hosting.
## Changes
### GitHub OAuth/App Setup Guide
- Updated
[github.mdx](cci:7://file:///Users/madison/source/stack-auth/docs/content/docs/%28guides%29/concepts/auth-providers/github.mdx:0:0-0:0)
with clearer instructions differentiating between **GitHub OAuth App**
and **GitHub App** setup
- Added better explanations for when to use each option
### Self-Hosting Documentation
- Added prominent danger warning about self-hosting responsibilities
- Migrated inline shell commands to structured code examples using
[PlatformCodeblock](cci:1://file:///Users/madison/source/stack-auth/docs/src/components/mdx/platform-codeblock.tsx:242:0-673:1)
component
- Created
[docs/code-examples/self-host.ts](cci:7://file:///Users/madison/source/stack-auth/docs/code-examples/self-host.ts:0:0-0:0)
with all self-hosting commands
### Info Component
- Added new `danger` type for critical warnings with red accent styling
- Updated component styling with modern left accent bar and gradient
backgrounds
### PlatformCodeblock Component
- Added `hidePlatformSelector` prop to hide platform dropdown for
single-platform code examples
- Added Shell platform support for terminal commands (Docker, Git, pnpm)
- Filtered Shell platform from user-selectable options in both the
codeblock and header selectors
### Platform Config
- Added Shell platform with Docker, Git, and pnpm frameworks
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added comprehensive self-hosting and authentication customization
example collections for copy-paste use.
* New "danger" info style with visual accent for important warnings.
* **Documentation**
* GitHub integration guide now centers on GitHub App with an alternate
OAuth path retained.
* Replaced many inline snippets with platform-driven code blocks and
improved platform/framework selector behavior (single-platform
optimization; option to hide selector).
* Pages now surface "Last updated" above descriptions.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
4d42f9aa66
commit
d0173af691
207
docs/code-examples/customization.ts
Normal file
207
docs/code-examples/customization.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { CodeExample } from '../lib/code-examples';
|
||||
|
||||
export const customizationExamples = {
|
||||
'sign-in': {
|
||||
'default': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
import { SignIn } from "@stackframe/stack";
|
||||
|
||||
export default function DefaultSignIn() {
|
||||
// optionally redirect to some other page if the user is already signed in
|
||||
// const user = useUser();
|
||||
// if (user) { redirect to some other page }
|
||||
return <SignIn fullPage />;
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-in/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
|
||||
'custom-oauth': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
|
||||
export default function CustomOAuthSignIn() {
|
||||
const app = useStackApp();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>My Custom Sign In page</h1>
|
||||
<button onClick={async () => {
|
||||
// This will redirect to the OAuth provider's login page.
|
||||
await app.signInWithOAuth('google');
|
||||
}}>
|
||||
Sign In with Google
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-in/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
|
||||
'custom-credential': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomCredentialSignIn() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password) {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignIn if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signInWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
<button type='submit'>Sign In</button>
|
||||
</form>
|
||||
);
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-in/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
|
||||
'custom-magic-link': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomMagicLinkSignIn() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!email) {
|
||||
setError('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
// This will send a magic link email to the user's email address.
|
||||
// When they click the link, they will be redirected to your application.
|
||||
const result = await app.sendMagicLinkEmail(email);
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
} else {
|
||||
setMessage('Magic link sent! Please check your email.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
{message ?
|
||||
<div>{message}</div> :
|
||||
<>
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<button type='submit'>Send Magic Link</button>
|
||||
</>}
|
||||
</form>
|
||||
);
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-in/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
},
|
||||
|
||||
'sign-up': {
|
||||
'default': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
import { SignUp } from "@stackframe/stack";
|
||||
|
||||
export default function DefaultSignUp() {
|
||||
// optionally redirect to some other page if the user is already signed in
|
||||
// const user = useUser();
|
||||
// if (user) { redirect to some other page }
|
||||
return <SignUp fullPage />;
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-up/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
|
||||
'custom-credential': [
|
||||
{
|
||||
language: 'JavaScript',
|
||||
framework: 'Next.js',
|
||||
code: `'use client';
|
||||
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomCredentialSignUp() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password) {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignUp if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signUpWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
<button type='submit'>Sign Up</button>
|
||||
</form>
|
||||
);
|
||||
}`,
|
||||
highlightLanguage: 'tsx',
|
||||
filename: 'app/handler/sign-up/page.tsx',
|
||||
},
|
||||
] as CodeExample[],
|
||||
},
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { CodeExample } from '../lib/code-examples';
|
||||
import { apiKeysExamples } from './api-keys';
|
||||
import { customizationExamples } from './customization';
|
||||
import { paymentsExamples } from './payments';
|
||||
import { selfHostExamples } from './self-host';
|
||||
import { setupExamples } from './setup';
|
||||
import { viteExamples } from './vite-example';
|
||||
|
||||
@ -8,9 +10,8 @@ const allExamples: Record<string, Record<string, Record<string, CodeExample[]>>>
|
||||
'setup': setupExamples,
|
||||
'apps': {...apiKeysExamples, ...paymentsExamples },
|
||||
'getting-started': viteExamples,
|
||||
// Add more sections here as needed:
|
||||
// 'auth': authExamples,
|
||||
// 'customization': customizationExamples,
|
||||
'others': selfHostExamples,
|
||||
'customization': customizationExamples,
|
||||
};
|
||||
|
||||
export function getExample(documentPath: string, exampleName: string): CodeExample[] | undefined {
|
||||
|
||||
106
docs/code-examples/self-host.ts
Normal file
106
docs/code-examples/self-host.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { CodeExample } from '../lib/code-examples';
|
||||
|
||||
export const selfHostExamples = {
|
||||
'self-host': {
|
||||
'docker-postgres': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'Docker',
|
||||
code: `docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'docker-run': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'Docker',
|
||||
code: `docker run --env-file <your-env-file.env> -p 8101:8101 -p 8102:8102 stackauth/server:latest`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'git-clone': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'Git',
|
||||
code: `git clone git@github.com:stack-auth/stack-auth.git
|
||||
cd stack-auth`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'local-dev-setup': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'pnpm',
|
||||
code: `pnpm install
|
||||
|
||||
# Run build to build everything once
|
||||
pnpm run build:dev
|
||||
|
||||
# reset & start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
|
||||
pnpm run start-deps
|
||||
# pnpm run restart-deps
|
||||
# pnpm run stop-deps
|
||||
|
||||
# Start the dev server
|
||||
pnpm run dev
|
||||
# For systems with limited resources, you can run a minimal development setup with just the backend and dashboard
|
||||
# pnpm run dev:basic
|
||||
|
||||
# In a different terminal, run tests in watch mode
|
||||
pnpm run test`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'prisma-studio': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'pnpm',
|
||||
code: `pnpm run prisma studio`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'backend-build': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'pnpm',
|
||||
code: `pnpm install
|
||||
pnpm build:backend
|
||||
pnpm start:backend`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'dashboard-build': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'pnpm',
|
||||
code: `pnpm install
|
||||
pnpm build:dashboard
|
||||
pnpm start:dashboard`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
|
||||
'db-init': [
|
||||
{
|
||||
language: 'Shell',
|
||||
framework: 'pnpm',
|
||||
code: `pnpm db:init`,
|
||||
highlightLanguage: 'bash',
|
||||
filename: 'Terminal'
|
||||
}
|
||||
] as CodeExample[],
|
||||
}
|
||||
};
|
||||
@ -1,8 +1,9 @@
|
||||
---
|
||||
title: "GitHub"
|
||||
lastModified: "2026-01-13"
|
||||
---
|
||||
|
||||
This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub OAuth allows users to sign in to your application using their GitHub account.
|
||||
This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub allows users to sign in to your Stack Auth-enabled app using their GitHub account.
|
||||
|
||||
<Info>
|
||||
For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page.
|
||||
@ -11,9 +12,40 @@ You should replace these before you go into production.
|
||||
|
||||
## Integration Steps
|
||||
|
||||
<Info>
|
||||
If you are unsure if you need to create a GitHub App, or a GitHub OAuth App, check the [Differences On GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps).
|
||||
More than likely, you will want to create a GitHub App. The installation process is the same for both.
|
||||
</Info>
|
||||
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create a GitHub OAuth App
|
||||
### Create a GitHub App
|
||||
|
||||
1. Navigate to your [GitHub Developer App Settings](https://github.com/settings/apps).
|
||||
2. Click the **New GitHub App** button.
|
||||
3. Enter a name for your application, homepage URL, and a description.
|
||||
4. For **Authorization callback URL**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/github`
|
||||
5. For permissions, at a **minimum**, you will need **Account Permissions > Email Addresses** set to **Read Only**. Your sign-in flow will not work without this permission.
|
||||
6. Select **Any Account** under the **_Where can this GitHub App be installed_** section.
|
||||
7. Click **Create GitHub App**
|
||||
8. Save the **Client ID** and click **Generate a new client secret** to create your **Client Secret**.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Enable GitHub Provider in Stack Auth
|
||||
|
||||
1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar.
|
||||
2. Click **Add SSO Providers** and select **GitHub** as the provider.
|
||||
3. Set the **Client ID** and **Client Secret** you obtained from your GitHub App earlier.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
<Accordion title="GitHub OAuth App installation">
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create an OAuth App
|
||||
|
||||
1. Navigate to your [GitHub Developer Settings](https://github.com/settings/developers).
|
||||
2. Click the **New OAuth App** button.
|
||||
@ -31,6 +63,7 @@ You should replace these before you go into production.
|
||||
3. Set the **Client ID** and **Client Secret** you obtained from GitHub earlier.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
### Need More Help?
|
||||
|
||||
|
||||
@ -1,24 +1,16 @@
|
||||
---
|
||||
title: Sign-In Page Examples
|
||||
lastModified: "2026-01-12"
|
||||
---
|
||||
|
||||
# Sign-In Page Examples
|
||||
|
||||
This page provides examples of how to create custom sign-in pages for your application using Stack Auth components and functions.
|
||||
|
||||
## Custom page with `SignIn` component
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { SignIn } from "@stackframe/stack";
|
||||
|
||||
export default function DefaultSignIn() {
|
||||
// optionally redirect to some other page if the user is already signed in
|
||||
// const user = useUser();
|
||||
// if (user) { redirect to some other page }
|
||||
return <SignIn fullPage />;
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-in"
|
||||
examples={["default"]}
|
||||
/>
|
||||
|
||||
You can also use `useUser` at the beginning of the sign-in page to check whether the user is already signed in and redirect them to another page if they are.
|
||||
|
||||
@ -34,108 +26,21 @@ You can also use `useUser` at the beginning of the sign-in page to check whether
|
||||
|
||||
## Custom OAuth Sign In
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
|
||||
export default function CustomOAuthSignIn() {
|
||||
const app = useStackApp();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>My Custom Sign In page</h1>
|
||||
<button onClick={async () => {
|
||||
// This will redirect to the OAuth provider's login page.
|
||||
await app.signInWithOAuth('google');
|
||||
}}>
|
||||
Sign In with Google
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-in"
|
||||
examples={["custom-oauth"]}
|
||||
/>
|
||||
|
||||
## Custom Credential Sign In
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomCredentialSignIn() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password) {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignIn if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signInWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
<button type='submit'>Sign In</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-in"
|
||||
examples={["custom-credential"]}
|
||||
/>
|
||||
|
||||
## Custom Magic Link Sign In
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomMagicLinkSignIn() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!email) {
|
||||
setError('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
// This will send a magic link email to the user's email address.
|
||||
// When they click the link, they will be redirected to your application.
|
||||
const result = await app.sendMagicLinkEmail(email);
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
} else {
|
||||
setMessage('Magic link sent! Please check your email.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
{message ?
|
||||
<div>{message}</div> :
|
||||
<>
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<button type='submit'>Send Magic Link</button>
|
||||
</>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-in"
|
||||
examples={["custom-magic-link"]}
|
||||
/>
|
||||
|
||||
@ -1,24 +1,17 @@
|
||||
---
|
||||
title: Sign-Up Page Examples
|
||||
lastModified: "2026-01-12"
|
||||
---
|
||||
|
||||
# Custom Sign-Up Page Examples
|
||||
|
||||
This page provides examples of how to create custom sign-up pages for your application using Stack Auth components and functions.
|
||||
|
||||
## Custom page with `SignUp` component
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { SignUp } from "@stackframe/stack";
|
||||
|
||||
export default function DefaultSignUp() {
|
||||
// optionally redirect to some other page if the user is already signed in
|
||||
// const user = useUser();
|
||||
// if (user) { redirect to some other page }
|
||||
return <SignUp fullPage />;
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-up"
|
||||
examples={["default"]}
|
||||
/>
|
||||
|
||||
You can also use `useUser` at the beginning of the sign-up page to check whether the user is already signed in and redirect them to another page if they are.
|
||||
|
||||
@ -36,43 +29,10 @@ OAuth sign-in and sign-up share the same function. Check out the [Sign In exampl
|
||||
|
||||
## Custom Credential Sign Up
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useStackApp } from "@stackframe/stack";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CustomCredentialSignUp() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const app = useStackApp();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!password) {
|
||||
setError('Please enter your password');
|
||||
return;
|
||||
}
|
||||
// This will redirect to app.urls.afterSignUp if successful.
|
||||
// You can customize the redirect URL in the StackServerApp constructor.
|
||||
const result = await app.signUpWithCredential({ email, password });
|
||||
// It's better to handle each error code separately, but for simplicity,
|
||||
// we'll just display the error message directly here.
|
||||
if (result.status === 'error') {
|
||||
setError(result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
|
||||
{error}
|
||||
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
<button type='submit'>Sign Up</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
<PlatformCodeblock
|
||||
document="customization/sign-up"
|
||||
examples={["custom-credential"]}
|
||||
/>
|
||||
|
||||
## Custom Magic Link Sign Up
|
||||
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
---
|
||||
title: Self-host
|
||||
lastModified: "2026-01-10"
|
||||
---
|
||||
|
||||
<Info type="danger">
|
||||
**If you self-host, YOU will be responsible for updating Stack Auth and its dependencies.** Security patches, bug fixes, and new features require manual updates on your infrastructure. If you would like premium support to help with this, [contact us](mailto:team@stack-auth.com).
|
||||
</Info>
|
||||
|
||||
Stack Auth is fully open-source and can be self-hosted on your own infrastructure. This guide will introduce each component of the project and how to set them up.
|
||||
|
||||
<Info>
|
||||
@ -31,16 +36,14 @@ On a high level, Stack Auth is composed of the following services:
|
||||
Stack Auth provides a [pre-configured Docker](https://hub.docker.com/r/stackauth/server) image that bundles the dashboard and API backend into a single container. To complete the setup, you'll need to provide your own PostgreSQL database, and optionally configure an email server and Svix instance for webhooks.
|
||||
|
||||
1. Use a cloud hosted Postgres or start a example Postgres database. Don't use this setting in production:
|
||||
```sh
|
||||
docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest
|
||||
```
|
||||
|
||||
<PlatformCodeblock document="others/self-host" examples={["docker-postgres"]} hidePlatformSelector />
|
||||
|
||||
2. Get the [example environment file](https://github.com/stack-auth/stack-auth/tree/main/docker/server/.env.example) and modify it to your needs (for security, you MUST edit at least the `STACK_SERVER_SECRET` value). You must also supply a `STACK_FREESTYLE_API_KEY` in order to send emails with Stack Auth (can be generated on [freestyle](https://freestyle.sh)). See the [full template here](https://github.com/stack-auth/stack-auth/blob/dev/docker/server/.env).
|
||||
|
||||
3. Run the Docker container:
|
||||
```sh
|
||||
docker run --env-file <your-env-file.env> -p 8101:8101 -p 8102:8102 stackauth/server:latest
|
||||
```
|
||||
|
||||
<PlatformCodeblock document="others/self-host" examples={["docker-run"]} hidePlatformSelector />
|
||||
|
||||
<Info>
|
||||
For M-series Mac users, you might need to add `--platform linux/x86_64` to the `docker run` command.
|
||||
@ -59,34 +62,13 @@ Now, login with your admin account on the dashboard and follow the [normal setup
|
||||
|
||||
Clone the repository and check out the directory:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:stack-auth/stack-auth.git
|
||||
cd stack
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
|
||||
|
||||
Pre-populated .env files for the setup below are available and used by default in `.env.development` in each of the packages. (Note: If you're creating a production build (eg. with `pnpm run build`), you must supply the environment variables manually.)
|
||||
|
||||
In a new terminal:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
|
||||
# Run build to build everything once
|
||||
pnpm run build:dev
|
||||
|
||||
# reset & start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
|
||||
pnpm run start-deps
|
||||
# pnpm run restart-deps
|
||||
# pnpm run stop-deps
|
||||
|
||||
# Start the dev server
|
||||
pnpm run dev
|
||||
# For systems with limited resources, you can run a minimal development setup with just the backend and dashboard
|
||||
# pnpm run dev:basic
|
||||
|
||||
# In a different terminal, run tests in watch mode
|
||||
pnpm run test
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["local-dev-setup"]} hidePlatformSelector />
|
||||
|
||||
You can now open the dev launchpad at [http://localhost:8100](http://localhost:8100). From there, you can navigate to the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106. See the dev launchpad for a list of all running services.
|
||||
|
||||
@ -94,9 +76,7 @@ Your IDE may show an error on all `@stackframe/XYZ` imports. To fix this, simply
|
||||
|
||||
You can also open Prisma Studio to see the database interface and edit data directly:
|
||||
|
||||
```sh
|
||||
pnpm run prisma studio
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["prisma-studio"]} hidePlatformSelector />
|
||||
|
||||
## Run individual services
|
||||
|
||||
@ -108,47 +88,31 @@ Deploy these services with your preferred platform. Copy the URLs/API keys—you
|
||||
|
||||
Clone the repository and check out the root directory:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:stack-auth/stack-auth.git
|
||||
cd stack
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
|
||||
|
||||
Set all the necessary environment variables (you can check out `apps/backend/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed domain (e.g., https://your-backend-url.com).
|
||||
|
||||
Build and start the server:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build:backend
|
||||
pnpm start:backend
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["backend-build"]} hidePlatformSelector />
|
||||
|
||||
### Dashboard
|
||||
|
||||
Clone the repository (if you are running it on a separate server, or skip this step if you are using the same server as the API backend) and check out the dashboard directory:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:stack-auth/stack-auth.git
|
||||
cd stack
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
|
||||
|
||||
Set all the necessary environment variables (you can check out `apps/dashboard/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed backend (e.g., https://your-backend-url.com).
|
||||
|
||||
Build and start the server:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm build:dashboard
|
||||
pnpm start:dashboard
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["dashboard-build"]} hidePlatformSelector />
|
||||
|
||||
### Initialize the database
|
||||
|
||||
You need to initialize the database with the following command with the backend environment variables set:
|
||||
|
||||
```sh
|
||||
pnpm db:init
|
||||
```
|
||||
<PlatformCodeblock document="others/self-host" examples={["db-init"]} hidePlatformSelector />
|
||||
|
||||
Now you can go to the dashboard (e.g., https://your-dashboard-url.com) and sign up for an account.
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* This ensures consistency between code examples, platform selectors, and the header indicator.
|
||||
*/
|
||||
|
||||
export type PlatformName = 'JavaScript' | 'Python';
|
||||
export type PlatformName = 'JavaScript' | 'Python' | 'Shell';
|
||||
export type FrameworkName = string;
|
||||
|
||||
export type PlatformConfig = {
|
||||
@ -29,6 +29,11 @@ export const PLATFORMS: PlatformConfig[] = [
|
||||
frameworks: ['Django', 'FastAPI', 'Flask'],
|
||||
defaultFramework: 'Django',
|
||||
},
|
||||
{
|
||||
name: 'Shell',
|
||||
frameworks: ['Docker', 'Git', 'pnpm'],
|
||||
defaultFramework: 'Docker',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -24,6 +24,18 @@ function getDefaultDocsRedirectUrl(): string | null {
|
||||
return firstDocsPage?.url ?? null;
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
// Parse date parts directly to avoid timezone issues
|
||||
// (new Date("2026-01-12") is interpreted as UTC, which can shift the day in local time)
|
||||
const [year, month, day] = dateString.split('-').map(Number);
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const suffix = day === 1 || day === 21 || day === 31 ? 'st'
|
||||
: day === 2 || day === 22 ? 'nd'
|
||||
: day === 3 || day === 23 ? 'rd'
|
||||
: 'th';
|
||||
return `${months[month - 1]} ${day}${suffix}, ${year}`;
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug?: string[] }>,
|
||||
}) {
|
||||
@ -46,7 +58,7 @@ export default async function Page(props: {
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<div className="flex flex-row items-center justify-between gap-4 mb-4">
|
||||
<div className="flex flex-row items-center justify-between gap-4 mb-2">
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
|
||||
@ -55,6 +67,11 @@ export default async function Page(props: {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{page.data.lastModified && (
|
||||
<p className="text-xs text-fd-muted-foreground/60 mb-4">
|
||||
Last updated {formatDate(page.data.lastModified)}
|
||||
</p>
|
||||
)}
|
||||
{/* Only show description if it exists and is not empty */}
|
||||
{page.data.description && page.data.description.trim() && (
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
@ -67,12 +84,6 @@ export default async function Page(props: {
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
{/* Show last modified date at the bottom, tucked away */}
|
||||
{page.data.lastModified && (
|
||||
<p className="text-xs text-fd-muted-foreground/50 mt-6">
|
||||
Last updated {page.data.lastModified}
|
||||
</p>
|
||||
)}
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
@ -163,7 +163,8 @@ export function PlatformIndicator({ className }: { className?: string }) {
|
||||
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wide text-fd-muted-foreground">
|
||||
Platform
|
||||
</div>
|
||||
{PLATFORMS.map((platformConfig) => (
|
||||
{/* Filter out Shell platform - it's only used internally for shell commands */}
|
||||
{PLATFORMS.filter(p => p.name !== 'Shell').map((platformConfig) => (
|
||||
<button
|
||||
key={platformConfig.name}
|
||||
onClick={(e) => {
|
||||
|
||||
@ -5,29 +5,35 @@ import { cn } from '../../lib/cn';
|
||||
|
||||
export type InfoProps = {
|
||||
children: React.ReactNode,
|
||||
type?: 'info' | 'warning' | 'success',
|
||||
type?: 'info' | 'warning' | 'success' | 'danger',
|
||||
size?: 'default' | 'small',
|
||||
}
|
||||
|
||||
export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
|
||||
const colorVariants = {
|
||||
info: {
|
||||
border: 'border-blue-400/30 dark:border-blue-400/20',
|
||||
bg: 'bg-blue-50/50 dark:bg-blue-900/10',
|
||||
accent: 'bg-gradient-to-b from-blue-400 to-blue-600',
|
||||
bg: 'bg-gradient-to-r from-blue-50/80 to-transparent dark:from-blue-950/30 dark:to-transparent',
|
||||
icon: 'text-blue-500 dark:text-blue-400',
|
||||
title: 'text-blue-700 dark:text-blue-300'
|
||||
title: 'text-blue-800 dark:text-blue-200'
|
||||
},
|
||||
warning: {
|
||||
border: 'border-amber-400/30 dark:border-amber-400/20',
|
||||
bg: 'bg-amber-50/50 dark:bg-amber-900/10',
|
||||
accent: 'bg-gradient-to-b from-amber-400 to-amber-600',
|
||||
bg: 'bg-gradient-to-r from-amber-50/80 to-transparent dark:from-amber-950/30 dark:to-transparent',
|
||||
icon: 'text-amber-500 dark:text-amber-400',
|
||||
title: 'text-amber-700 dark:text-amber-300'
|
||||
title: 'text-amber-800 dark:text-amber-200'
|
||||
},
|
||||
success: {
|
||||
border: 'border-emerald-400/30 dark:border-emerald-400/20',
|
||||
bg: 'bg-emerald-50/50 dark:bg-emerald-900/10',
|
||||
accent: 'bg-gradient-to-b from-emerald-400 to-emerald-600',
|
||||
bg: 'bg-gradient-to-r from-emerald-50/80 to-transparent dark:from-emerald-950/30 dark:to-transparent',
|
||||
icon: 'text-emerald-500 dark:text-emerald-400',
|
||||
title: 'text-emerald-700 dark:text-emerald-300'
|
||||
title: 'text-emerald-800 dark:text-emerald-200'
|
||||
},
|
||||
danger: {
|
||||
accent: 'bg-gradient-to-b from-red-500 to-red-700',
|
||||
bg: 'bg-gradient-to-r from-red-50/90 to-transparent dark:from-red-950/40 dark:to-transparent',
|
||||
icon: 'text-red-600 dark:text-red-400',
|
||||
title: 'text-red-900 dark:text-red-200'
|
||||
}
|
||||
};
|
||||
|
||||
@ -51,12 +57,14 @@ export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative overflow-hidden rounded-lg',
|
||||
'border border-dashed',
|
||||
'shadow-sm',
|
||||
sizes.container,
|
||||
colors.border,
|
||||
colors.bg
|
||||
)}>
|
||||
{/* Left accent bar */}
|
||||
<div className={cn(
|
||||
'absolute left-0 top-0 bottom-0 w-1 rounded-l-lg',
|
||||
colors.accent
|
||||
)} />
|
||||
<div className={cn("flex items-baseline", sizes.content)}>
|
||||
<div className={cn("flex-shrink-0", colors.icon)}>
|
||||
{type === 'info' && (
|
||||
@ -74,6 +82,11 @@ export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
|
||||
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
{type === 'danger' && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={sizes.icon}>
|
||||
<path fillRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("flex-1", colors.title)}>
|
||||
{children}
|
||||
|
||||
@ -124,6 +124,11 @@ export type PlatformCodeblockProps = {
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string,
|
||||
/**
|
||||
* Hide the platform/framework selector dropdown
|
||||
* Useful for single-platform examples like shell commands
|
||||
*/
|
||||
hidePlatformSelector?: boolean,
|
||||
}
|
||||
|
||||
type VariantConfig = {
|
||||
@ -239,7 +244,8 @@ export function PlatformCodeblock({
|
||||
document: documentPath,
|
||||
examples: exampleNames,
|
||||
title,
|
||||
className
|
||||
className,
|
||||
hidePlatformSelector = false
|
||||
}: PlatformCodeblockProps) {
|
||||
// Load and convert examples from the centralized code-examples.ts file
|
||||
const allExamples: CodeExample[] = [];
|
||||
@ -258,8 +264,11 @@ export function PlatformCodeblock({
|
||||
? convertExamplesToPlatforms(allExamples)
|
||||
: { platforms: {}, defaultPlatform: '', defaultFrameworks: {}, defaultVariants: {}, variantKeys: {} };
|
||||
|
||||
const platformNames = Object.keys(platforms);
|
||||
const firstPlatform = defaultPlatform || platformNames[0];
|
||||
// Get all platform names but filter out Shell - it's only used internally for shell commands
|
||||
// and shouldn't be shown as a user-selectable option in the platform dropdown
|
||||
const allPlatformNames = Object.keys(platforms);
|
||||
const platformNames = allPlatformNames.filter(p => p !== 'Shell');
|
||||
const firstPlatform = defaultPlatform || platformNames[0] || allPlatformNames[0];
|
||||
|
||||
// Initialize with global platform or default
|
||||
// Important: This must return the same value on server and client for hydration
|
||||
@ -526,7 +535,8 @@ export function PlatformCodeblock({
|
||||
}));
|
||||
};
|
||||
|
||||
if (platformNames.length === 0) {
|
||||
// Check if there are ANY platforms (including Shell) - not just user-selectable ones
|
||||
if (allPlatformNames.length === 0) {
|
||||
return <div className="text-fd-muted-foreground">No platforms configured</div>;
|
||||
}
|
||||
|
||||
@ -540,92 +550,95 @@ export function PlatformCodeblock({
|
||||
title={title}
|
||||
filename={currentCodeConfig?.filename}
|
||||
headerContent={
|
||||
<div className="relative ml-auto">
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className={cn(
|
||||
// Hide selector when explicitly requested, when there's only one platform with one framework, or when there are no user-selectable platforms (Shell-only)
|
||||
hidePlatformSelector || platformNames.length === 0 || (platformNames.length === 1 && currentFrameworks.length === 1) ? undefined : (
|
||||
<div className="relative ml-auto">
|
||||
<button
|
||||
onClick={toggleDropdown}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-lg border border-fd-border/70 bg-fd-background/80 px-3 py-1.5 text-sm font-medium text-fd-foreground shadow-sm transition-all duration-150",
|
||||
"hover:border-fd-primary/50 hover:bg-fd-primary/5 hover:shadow-md"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{selectedPlatform} / {currentFramework}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{selectedPlatform} / {currentFramework}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3 w-3 text-fd-muted-foreground transition-transform duration-200",
|
||||
isDropdownOpen && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 top-full z-[200] mt-1 min-w-[220px] rounded-lg border border-fd-border/70 bg-fd-background shadow-lg">
|
||||
{dropdownView === 'platform' ? (
|
||||
<div className="py-1">
|
||||
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
|
||||
Choose Platform
|
||||
</div>
|
||||
{platformNames.map((platform) => (
|
||||
<button
|
||||
key={platform}
|
||||
onClick={(e) => {
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute right-0 top-full z-[200] mt-1 min-w-[220px] rounded-lg border border-fd-border/70 bg-fd-background shadow-lg">
|
||||
{dropdownView === 'platform' ? (
|
||||
<div className="py-1">
|
||||
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
|
||||
Choose Platform
|
||||
</div>
|
||||
{platformNames.map((platform) => (
|
||||
<button
|
||||
key={platform}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePlatformSelect(platform);
|
||||
}}
|
||||
className={cn(
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between px-3 py-1.5 text-sm transition-all duration-150",
|
||||
"hover:bg-fd-primary/10 hover:text-fd-primary hover:font-medium",
|
||||
selectedPlatform === platform
|
||||
? "bg-fd-primary/15 text-fd-primary font-semibold"
|
||||
: "text-fd-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span>{platform}</span>
|
||||
<ChevronDown className="h-3 w-3 -rotate-90 text-fd-muted-foreground/80" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
<div className="flex items-center px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
|
||||
<button
|
||||
onClick={() => setDropdownView('platform')}
|
||||
className="mr-2 flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground hover:text-fd-primary"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3 rotate-90" />
|
||||
Back
|
||||
</button>
|
||||
<span>Select {selectedPlatform} framework</span>
|
||||
>
|
||||
<span>{platform}</span>
|
||||
<ChevronDown className="h-3 w-3 -rotate-90 text-fd-muted-foreground/80" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{currentFrameworks.map((framework) => (
|
||||
<button
|
||||
key={framework}
|
||||
onClick={(e) => {
|
||||
) : (
|
||||
<div className="py-1">
|
||||
<div className="flex items-center px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
|
||||
<button
|
||||
onClick={() => setDropdownView('platform')}
|
||||
className="mr-2 flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground hover:text-fd-primary"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3 rotate-90" />
|
||||
Back
|
||||
</button>
|
||||
<span>Select {selectedPlatform} framework</span>
|
||||
</div>
|
||||
{currentFrameworks.map((framework) => (
|
||||
<button
|
||||
key={framework}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleFrameworkSelect(framework);
|
||||
}}
|
||||
className={cn(
|
||||
}}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm text-left",
|
||||
"hover:bg-fd-primary/10 hover:text-fd-primary hover:font-medium",
|
||||
currentFramework === framework
|
||||
? "bg-fd-primary/15 text-fd-primary font-semibold"
|
||||
: "text-fd-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{framework}
|
||||
{currentFramework === framework && (
|
||||
<span className="text-[10px] text-fd-primary/70 font-medium">current</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{framework}
|
||||
{currentFramework === framework && (
|
||||
<span className="text-[10px] text-fd-primary/70 font-medium">current</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
beforeCodeContent={
|
||||
hasVariants(selectedPlatform, currentFramework) ? (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user