mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 19:38:53 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0f2be1469 | |||
| 09a3284682 | |||
| 4845af3f2c | |||
| 6b0f70a80c | |||
| e2ad434b93 | |||
| 000b950ee6 | |||
| 7562072cd4 | |||
| 9814e87a41 | |||
| c7c5a28395 |
@@ -0,0 +1,26 @@
|
||||
name: Build formbricks-com
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Formbricks-com
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Build Formbricks-com
|
||||
run: pnpm build --filter=formbricks-com...
|
||||
@@ -0,0 +1,131 @@
|
||||
name: Kamal Deploy
|
||||
concurrency:
|
||||
group: deploy-to-kamal
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
Deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
|
||||
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
|
||||
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
|
||||
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
|
||||
TERMS_URL: ${{ vars.TERMS_URL }}
|
||||
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
|
||||
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
|
||||
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
|
||||
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
|
||||
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
|
||||
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
|
||||
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
|
||||
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
|
||||
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
|
||||
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
|
||||
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
|
||||
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
|
||||
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
|
||||
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
|
||||
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
|
||||
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
|
||||
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
|
||||
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
|
||||
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
|
||||
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
|
||||
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
NODE_ENV: production
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||
S3_REGION: ${{ vars.S3_REGION }}
|
||||
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.0
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
gem install kamal
|
||||
|
||||
- uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Create builder
|
||||
run: docker buildx create --use --name formbricks-gh-actions-builder
|
||||
if: steps.buildx.outputs.should_create_builder == 'true'
|
||||
|
||||
- name: Push env variables to Kamal
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
kamal env push
|
||||
|
||||
- name: Run deploy command
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
set +e
|
||||
DEPLOY_OUTPUT=$(kamal deploy 2>&1)
|
||||
DEPLOY_EXIT_CODE=$?
|
||||
echo "$DEPLOY_OUTPUT"
|
||||
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
|
||||
echo "Deployment reported healthy container. Considering as success."
|
||||
kamal lock release
|
||||
exit 0
|
||||
else
|
||||
exit $DEPLOY_EXIT_CODE
|
||||
fi
|
||||
shell: bash
|
||||
@@ -0,0 +1,128 @@
|
||||
name: Kamal Setup
|
||||
concurrency:
|
||||
group: setup-kamal
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Only to be triggered when accessories are updated
|
||||
|
||||
jobs:
|
||||
Setup:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
|
||||
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
|
||||
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
|
||||
MAIL_FROM: ${{ secrets.MAIL_FROM }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
|
||||
TERMS_URL: ${{ vars.TERMS_URL }}
|
||||
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
|
||||
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
|
||||
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
|
||||
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
|
||||
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
|
||||
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
|
||||
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
|
||||
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
|
||||
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
|
||||
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
|
||||
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
|
||||
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
|
||||
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
|
||||
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
|
||||
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
|
||||
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
|
||||
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
|
||||
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
|
||||
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
|
||||
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
|
||||
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
NODE_ENV: production
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
|
||||
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
|
||||
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
||||
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
||||
S3_REGION: ${{ vars.S3_REGION }}
|
||||
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.0
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
gem install kamal
|
||||
|
||||
- uses: webfactory/ssh-agent@v0.9.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Create builder
|
||||
run: docker buildx create --use --name formbricks-gh-actions-builder
|
||||
if: steps.buildx.outputs.should_create_builder == 'true'
|
||||
|
||||
- name: Push env variables to Kamal
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
kamal env push
|
||||
|
||||
- name: Run setup command
|
||||
run: |
|
||||
kamal() { command kamal "$@" -c kamal/deploy.yml; }
|
||||
set +e
|
||||
DEPLOY_OUTPUT=$(kamal setup 2>&1)
|
||||
DEPLOY_EXIT_CODE=$?
|
||||
echo "$DEPLOY_OUTPUT"
|
||||
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
|
||||
echo "Deployment reported healthy container. Considering as success."
|
||||
kamal lock release
|
||||
exit 0
|
||||
else
|
||||
exit $DEPLOY_EXIT_CODE
|
||||
fi
|
||||
shell: bash
|
||||
@@ -1,4 +1,9 @@
|
||||
name: Docker Release to GitHub
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -48,17 +53,6 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set tags based on event type
|
||||
id: set-tags
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" ]]; then
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "::set-output name=tags::latest,${{ github.ref }}"
|
||||
fi
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "::set-output name=tags::experimental"
|
||||
fi
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
@@ -66,7 +60,6 @@ jobs:
|
||||
uses: docker/metadata-action@v5 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ steps.set-tags.outputs.tags }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Release on Dockerhub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release-image-on-dockerhub:
|
||||
name: Release on Dockerhub
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Get Release Tag
|
||||
id: extract_release_tag
|
||||
run: |
|
||||
TAG=${{ github.ref }}
|
||||
TAG=${TAG#refs/tags/v}
|
||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/web/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
export const LayoutApp = ({ children }: { children: React.ReactNode }) => {
|
||||
export default function LayoutApp({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-full">
|
||||
{/* Static sidebar for desktop */}
|
||||
@@ -10,4 +10,4 @@ export const LayoutApp = ({ children }: { children: React.ReactNode }) => {
|
||||
<div className="flex flex-1 flex-col lg:pl-64">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const secondaryNavigation = [
|
||||
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
|
||||
];
|
||||
|
||||
export const Sidebar = () => {
|
||||
export default function Sidebar({}) {
|
||||
return (
|
||||
<div className="flex flex-grow flex-col overflow-y-auto bg-cyan-700 pb-4 pt-5">
|
||||
<nav
|
||||
@@ -63,4 +63,4 @@ export const Sidebar = () => {
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const classNames = (...classes: any) => {
|
||||
export function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -18,6 +18,4 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
const Document = () => {
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en" className="h-full bg-slate-50">
|
||||
<Head />
|
||||
@@ -10,6 +10,4 @@ const Document = () => {
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Document;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import fbsetup from "../../public/fb-setup.png";
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const AppPage = ({}) => {
|
||||
export default function AppPage({}) {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
@@ -36,11 +36,7 @@ const AppPage = ({}) => {
|
||||
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
|
||||
const userInitAttributes = {
|
||||
language: "de",
|
||||
"Init Attribute 1": "eight",
|
||||
"Init Attribute 2": "two",
|
||||
};
|
||||
const userInitAttributes = { language: "de", "Init Attribute 1": "eight", "Init Attribute 2": "two" };
|
||||
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
@@ -238,6 +234,4 @@ const AppPage = ({}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppPage;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import fbsetup from "../../public/fb-setup.png";
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const AppPage = ({}) => {
|
||||
export default function AppPage({}) {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
@@ -139,6 +139,4 @@ const AppPage = ({}) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppPage;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const libraries = [
|
||||
},
|
||||
];
|
||||
|
||||
export const Libraries = () => {
|
||||
export function Libraries() {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-slate-900/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3 dark:border-white/5">
|
||||
@@ -57,4 +57,4 @@ export const Libraries = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,11 +80,6 @@ formbricks.init({
|
||||
|
||||
You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.):
|
||||
|
||||
<Note>
|
||||
Please note that the number of different attribute classes (e.g., "Plan," "First Name," etc.) is currently
|
||||
limited to 150 attributes per environment.
|
||||
</Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setting Custom Attributes">
|
||||
|
||||
@@ -120,4 +115,4 @@ formbricks.logout();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Col>
|
||||
@@ -135,7 +135,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover"
|
||||
|
||||
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
|
||||
|
||||
export const DocsFeedback = () => {
|
||||
export default function DocsFeedback() {
|
||||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [sharedFeedback, setSharedFeedback] = useState(false);
|
||||
@@ -199,7 +199,7 @@ export const DocsFeedback = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
|
||||
|
||||
import { FaqJsonLdComponent } from "./FAQPageJsonLd";
|
||||
import FaqJsonLdComponent from "./FAQPageJsonLd";
|
||||
|
||||
const FAQ_DATA = [
|
||||
{
|
||||
@@ -62,7 +62,7 @@ export const faqJsonLdData = FAQ_DATA.map((faq) => ({
|
||||
acceptedAnswerText: faq.answer(),
|
||||
}));
|
||||
|
||||
export const FAQ = () => {
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<>
|
||||
<FaqJsonLdComponent data={faqJsonLdData} />
|
||||
@@ -76,4 +76,4 @@ export const FAQ = () => {
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { FAQPageJsonLd } from "next-seo";
|
||||
|
||||
export const FaqJsonLdComponent = ({ data }) => {
|
||||
export default function FaqJsonLdComponent({ data }) {
|
||||
const faqEntities = data.map(({ question, answer }) => ({
|
||||
questionName: question,
|
||||
acceptedAnswerText: answer,
|
||||
}));
|
||||
|
||||
return <FAQPageJsonLd mainEntity={faqEntities} />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ export const metadata = {
|
||||
The Airtable integration allows you to automatically send responses to an Airtable of your choice.
|
||||
|
||||
<Note>
|
||||
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
@@ -21,7 +21,9 @@ export const metadata = {
|
||||
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
|
||||
|
||||
<Note>
|
||||
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks. For self-configuration, see additional setup
|
||||
[below](#setup-in-self-hosted-formbricks).
|
||||
</Note>
|
||||
|
||||
## Connect Google Sheets
|
||||
|
||||
@@ -27,7 +27,7 @@ export const metadata = {
|
||||
Make is a powerful tool to send information between Formbricks and thousands of apps. Here's how to set it up.
|
||||
|
||||
<Note>
|
||||
Nailed down your survey?? Any changes in the survey cause additional work in the _Scenario_. It
|
||||
### Nail down your survey first ? Any changes in the survey cause additional work in the _Scenario_. It
|
||||
makes sense to first settle on the survey you want to run and then get to setting up Make.
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const metadata = {
|
||||
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
|
||||
|
||||
<Note>
|
||||
Nail down your survey? Any changes in the survey cause additional work in the n8n node. It makes
|
||||
### Nail down your survey first Any changes in the survey cause additional work in the n8n node. It makes
|
||||
sense to first settle on the survey you want to run and then get to setting up n8n.
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const metadata = {
|
||||
The notion integration allows you to automatically send responses to a Notion database of your choice.
|
||||
|
||||
<Note>
|
||||
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
@@ -10,7 +10,7 @@ export const metadata = {
|
||||
At Formbricks, we understand the importance of integrating with third-party applications. We have step-by-step guides to configure our third-party integrations with a your Formbricks instance. We currently support the below integrations, click on them to see their individual guides:
|
||||
|
||||
<Note>
|
||||
If you are on a self-hosted instance, you will need to configure these integrations manually. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||
If you are on a self-hosted instance, you will need to configure these integrations manually. Please follow the guides [here](/self-hosting/integrations) to configure them.
|
||||
</Note>
|
||||
|
||||
- [Airtable](/developer-docs/integrations/airtable): Automatically send responses to an Airtable of your choice.
|
||||
|
||||
@@ -22,7 +22,8 @@ export const metadata = {
|
||||
The slack integration allows you to automatically send responses to a Slack channel of your choice.
|
||||
|
||||
<Note>
|
||||
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
@@ -26,7 +26,7 @@ const gettingStarted = [
|
||||
},
|
||||
];
|
||||
|
||||
export const GettingStarted = () => {
|
||||
export function GettingStarted() {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<Heading level={2} id="getting-started">
|
||||
@@ -47,4 +47,4 @@ export const GettingStarted = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const metadata: Metadata = {
|
||||
|
||||
const jost = Jost({ subsets: ["latin"] });
|
||||
|
||||
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
let pages = await glob("**/*.mdx", { cwd: "src/app" });
|
||||
let allSectionsEntries = (await Promise.all(
|
||||
pages.map(async (filename) => [
|
||||
@@ -36,6 +36,4 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { HeroPattern } from "@/components/HeroPattern";
|
||||
|
||||
const NotFound = () => {
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<HeroPattern />
|
||||
@@ -17,6 +17,4 @@ const NotFound = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const ThemeWatcher = () => {
|
||||
function ThemeWatcher() {
|
||||
let { resolvedTheme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
let media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
const onMediaChange = () => {
|
||||
function onMediaChange() {
|
||||
let systemTheme = media.matches ? "dark" : "light";
|
||||
if (resolvedTheme === systemTheme) {
|
||||
setTheme("system");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMediaChange();
|
||||
media.addEventListener("change", onMediaChange);
|
||||
@@ -25,13 +25,13 @@ const ThemeWatcher = () => {
|
||||
}, [resolvedTheme, setTheme]);
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange>
|
||||
<ThemeWatcher />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const metadata = {
|
||||
|
||||
# Advanced Setup
|
||||
|
||||
Quickly set up and start using Formbricks with our [official Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) that we've already built for you.
|
||||
Quickly set up and start using Formbricks with our [official Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) that we've already built for you.
|
||||
|
||||
The pre-built image is ready-to-run, and it only requires minimal configuration on your part. It's as easy as downloading the Docker image and firing up the container.
|
||||
|
||||
@@ -104,25 +104,7 @@ You're now ready to start the Formbricks Docker setup. The following command wil
|
||||
|
||||
## Update
|
||||
|
||||
<Note>
|
||||
Please take a look at our [migration guide](/self-hosting/migration-guide) for version specific steps to
|
||||
update Formbricks.
|
||||
</Note>
|
||||
|
||||
1. Pull the latest Formbricks image
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Pull the changes into docker">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
|
||||
2. Stop the Formbricks stack
|
||||
1. Stop the Formbricks stack
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the docker instance">
|
||||
@@ -135,7 +117,21 @@ You're now ready to start the Formbricks Docker setup. The following command wil
|
||||
|
||||
</Col>
|
||||
|
||||
3. Re-start the Formbricks stack with the updated image
|
||||
2. Pull the latest changes
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Pull the changes into docker">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
|
||||
3. Update env vars as necessary in the docker-compose file.
|
||||
4. Re-start the Formbricks stack
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Relaunch the Docker Instance">
|
||||
|
||||
@@ -39,10 +39,10 @@ We have step-by-step guides to configure our third-party integrations with a sel
|
||||
- [Airtable](#airtable)
|
||||
- [Google Sheets](#google-sheets)
|
||||
- [Notion](#notion)
|
||||
- Make: We do not support for self-hosted instances yet.
|
||||
- Make: We do not support [Make.com](http://Make.com) for Self-hosted instances yet! Please follow our Cloud guide [here](/integrations#make)
|
||||
- [n8n](#n8n)
|
||||
- [Slack](#slack)
|
||||
- Wordpress: Wordpress setup is similar to the [Cloud setup](/developer-docs/integrations/wordpress), just change the API Host to your self-hosted URL.
|
||||
- [Wordpress]: Wordpress setup is similar to steps mentioned in Cloud [here](/integrations#wordpress), just change the API Host to your self-hosted URL.
|
||||
- [Zapier](#zapier)
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -17,14 +17,6 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva
|
||||
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v2.0.
|
||||
</Note>
|
||||
|
||||
and
|
||||
|
||||
<Note>
|
||||
If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be
|
||||
downgraded to the Community Edition 2.0. You find all license details on the [license
|
||||
page.](/self-hosting/license/)
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
|
||||
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
|
||||
@@ -37,7 +29,7 @@ To run all these steps, please navigate to the `formbricks` folder where your `d
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump
|
||||
docker exec formbricks-quickstart-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -53,19 +45,7 @@ docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbr
|
||||
restore scenario you will need to use `psql` then with an empty `formbricks` database.
|
||||
</Note>
|
||||
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
2. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
@@ -77,7 +57,7 @@ docker-compose down
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
3. Restarting the containers will automatically pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
@@ -89,7 +69,7 @@ docker-compose up -d
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
4. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
|
||||
|
||||
@@ -97,7 +77,6 @@ docker-compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/formbricks/data-migrations:latest && \
|
||||
docker run --rm \
|
||||
--network=formbricks_default \
|
||||
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
|
||||
@@ -110,7 +89,7 @@ docker run --rm \
|
||||
|
||||
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
|
||||
|
||||
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
5. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
|
||||
### App Surveys with @formbricks/js
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const libraries = [
|
||||
},
|
||||
];
|
||||
|
||||
export const Libraries = () => {
|
||||
export function Libraries() {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-slate-900/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3 dark:border-white/5">
|
||||
@@ -57,4 +57,4 @@ export const Libraries = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
const ArrowIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function ArrowIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -12,7 +12,7 @@ const ArrowIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const variantStyles = {
|
||||
primary:
|
||||
@@ -34,7 +34,7 @@ type ButtonProps = {
|
||||
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
|
||||
);
|
||||
|
||||
export const Button = ({ variant = "primary", className, children, arrow, ...props }: ButtonProps) => {
|
||||
export function Button({ variant = "primary", className, children, arrow, ...props }: ButtonProps) {
|
||||
className = clsx(
|
||||
"inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center",
|
||||
variantStyles[variant],
|
||||
@@ -74,4 +74,4 @@ export const Button = ({ variant = "primary", className, children, arrow, ...pro
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const languageNames: Record<string, string> = {
|
||||
go: "Go",
|
||||
};
|
||||
|
||||
const getPanelTitle = ({ title, language }: { title?: string; language?: string }) => {
|
||||
function getPanelTitle({ title, language }: { title?: string; language?: string }) {
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
@@ -25,9 +25,9 @@ const getPanelTitle = ({ title, language }: { title?: string; language?: string
|
||||
return languageNames[language];
|
||||
}
|
||||
return "Code";
|
||||
};
|
||||
}
|
||||
|
||||
const ClipboardIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function ClipboardIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -41,9 +41,9 @@ const ClipboardIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CopyButton = ({ code }: { code: string }) => {
|
||||
function CopyButton({ code }: { code: string }) {
|
||||
let [copyCount, setCopyCount] = useState(0);
|
||||
let copied = copyCount > 0;
|
||||
|
||||
@@ -89,9 +89,9 @@ const CopyButton = ({ code }: { code: string }) => {
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CodePanelHeader = ({ tag, label }: { tag?: string; label?: string }) => {
|
||||
function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) {
|
||||
if (!tag && !label) {
|
||||
return null;
|
||||
}
|
||||
@@ -107,9 +107,9 @@ const CodePanelHeader = ({ tag, label }: { tag?: string; label?: string }) => {
|
||||
{label && <span className="font-mono text-xs text-slate-400">{label}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CodePanel = ({
|
||||
function CodePanel({
|
||||
children,
|
||||
tag,
|
||||
label,
|
||||
@@ -119,7 +119,7 @@ const CodePanel = ({
|
||||
tag?: string;
|
||||
label?: string;
|
||||
code?: string;
|
||||
}) => {
|
||||
}) {
|
||||
let child = Children.only(children);
|
||||
|
||||
if (isValidElement(child)) {
|
||||
@@ -141,9 +141,9 @@ const CodePanel = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CodeGroupHeader = ({
|
||||
function CodeGroupHeader({
|
||||
title,
|
||||
children,
|
||||
selectedIndex,
|
||||
@@ -151,7 +151,7 @@ const CodeGroupHeader = ({
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
selectedIndex: number;
|
||||
}) => {
|
||||
}) {
|
||||
let hasTabs = Children.count(children) >= 1;
|
||||
|
||||
if (!title && !hasTabs) {
|
||||
@@ -178,9 +178,9 @@ const CodeGroupHeader = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CodeGroupPanels = ({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodePanel>) => {
|
||||
function CodeGroupPanels({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodePanel>) {
|
||||
let hasTabs = Children.count(children) >= 1;
|
||||
|
||||
if (hasTabs) {
|
||||
@@ -196,9 +196,9 @@ const CodeGroupPanels = ({ children, ...props }: React.ComponentPropsWithoutRef<
|
||||
}
|
||||
|
||||
return <CodePanel {...props}>{children}</CodePanel>;
|
||||
};
|
||||
}
|
||||
|
||||
const usePreventLayoutShift = () => {
|
||||
function usePreventLayoutShift() {
|
||||
let positionRef = useRef<HTMLElement>(null);
|
||||
let rafRef = useRef<number>();
|
||||
|
||||
@@ -227,7 +227,7 @@ const usePreventLayoutShift = () => {
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const usePreferredLanguageStore = create<{
|
||||
preferredLanguages: Array<string>;
|
||||
@@ -243,7 +243,7 @@ const usePreferredLanguageStore = create<{
|
||||
})),
|
||||
}));
|
||||
|
||||
const useTabGroupProps = (availableLanguages: Array<string>) => {
|
||||
function useTabGroupProps(availableLanguages: Array<string>) {
|
||||
let { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore();
|
||||
let [selectedIndex, setSelectedIndex] = useState(0);
|
||||
let activeLanguage = [...availableLanguages].sort(
|
||||
@@ -265,15 +265,15 @@ const useTabGroupProps = (availableLanguages: Array<string>) => {
|
||||
preventLayoutShift(() => addPreferredLanguage(availableLanguages[newSelectedIndex]));
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const CodeGroupContext = createContext(false);
|
||||
|
||||
export const CodeGroup = ({
|
||||
export function CodeGroup({
|
||||
children,
|
||||
title,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof CodeGroupPanels> & { title: string }) => {
|
||||
}: React.ComponentPropsWithoutRef<typeof CodeGroupPanels> & { title: string }) {
|
||||
let languages =
|
||||
Children.map(children, (child) => getPanelTitle(isValidElement(child) ? child.props : {})) ?? [];
|
||||
let tabGroupProps = useTabGroupProps(languages);
|
||||
@@ -303,9 +303,9 @@ export const CodeGroup = ({
|
||||
)}
|
||||
</CodeGroupContext.Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Code = ({ children, ...props }: React.ComponentPropsWithoutRef<"code">) => {
|
||||
export function Code({ children, ...props }: React.ComponentPropsWithoutRef<"code">) {
|
||||
let isGrouped = useContext(CodeGroupContext);
|
||||
|
||||
if (isGrouped) {
|
||||
@@ -316,9 +316,9 @@ export const Code = ({ children, ...props }: React.ComponentPropsWithoutRef<"cod
|
||||
}
|
||||
|
||||
return <code {...props}>{children}</code>;
|
||||
};
|
||||
}
|
||||
|
||||
export const Pre = ({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodeGroup>) => {
|
||||
export function Pre({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodeGroup>) {
|
||||
let isGrouped = useContext(CodeGroupContext);
|
||||
|
||||
if (isGrouped) {
|
||||
@@ -326,4 +326,4 @@ export const Pre = ({ children, ...props }: React.ComponentPropsWithoutRef<typeo
|
||||
}
|
||||
|
||||
return <CodeGroup {...props}>{children}</CodeGroup>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { Fragment, forwardRef, useState } from "react";
|
||||
|
||||
const CheckIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function CheckIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<circle cx="10" cy="10" r="10" strokeWidth="0" />
|
||||
@@ -16,9 +16,9 @@ const CheckIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const FeedbackButton = (props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">) => {
|
||||
function FeedbackButton(props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">) {
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
@@ -26,12 +26,12 @@ const FeedbackButton = (props: Omit<React.ComponentPropsWithoutRef<"button">, "t
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const FeedbackForm = forwardRef<
|
||||
React.ElementRef<"form">,
|
||||
Pick<React.ComponentPropsWithoutRef<"form">, "onSubmit">
|
||||
>(({ onSubmit }, ref) => {
|
||||
>(function FeedbackForm({ onSubmit }, ref) {
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
@@ -47,9 +47,7 @@ const FeedbackForm = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
FeedbackForm.displayName = "FeedbackForm";
|
||||
|
||||
const FeedbackThanks = forwardRef<React.ElementRef<"div">>((_props, ref) => {
|
||||
const FeedbackThanks = forwardRef<React.ElementRef<"div">>(function FeedbackThanks(_props, ref) {
|
||||
return (
|
||||
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
|
||||
<div className="flex items-center gap-3 rounded-full bg-teal-50/50 py-1 pl-1.5 pr-3 text-sm text-teal-900 ring-1 ring-inset ring-teal-500/20 dark:bg-teal-500/5 dark:text-teal-200 dark:ring-teal-500/30">
|
||||
@@ -60,19 +58,17 @@ const FeedbackThanks = forwardRef<React.ElementRef<"div">>((_props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
FeedbackThanks.displayName = "FeedbackThanks";
|
||||
|
||||
export const Feedback = () => {
|
||||
export function Feedback() {
|
||||
let [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
// event.nativeEvent.submitter.dataset.response
|
||||
// => "yes" or "no"
|
||||
|
||||
setSubmitted(true);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-8">
|
||||
@@ -94,4 +90,4 @@ export const Feedback = () => {
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DiscordIcon } from "./icons/DiscordIcon";
|
||||
import { GithubIcon } from "./icons/GithubIcon";
|
||||
import { TwitterIcon } from "./icons/TwitterIcon";
|
||||
|
||||
const PageLink = ({
|
||||
function PageLink({
|
||||
label,
|
||||
page,
|
||||
previous = false,
|
||||
@@ -17,7 +17,7 @@ const PageLink = ({
|
||||
label: string;
|
||||
page: { href: string; title: string };
|
||||
previous?: boolean;
|
||||
}) => {
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@@ -36,9 +36,9 @@ const PageLink = ({
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const PageNavigation = () => {
|
||||
function PageNavigation() {
|
||||
let pathname = usePathname();
|
||||
let allPages = navigation.flatMap((group) => {
|
||||
return group.links.flatMap((link) => {
|
||||
@@ -72,9 +72,9 @@ const PageNavigation = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const SocialLink = ({
|
||||
function SocialLink({
|
||||
href,
|
||||
icon: Icon,
|
||||
children,
|
||||
@@ -82,16 +82,16 @@ const SocialLink = ({
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
}) {
|
||||
return (
|
||||
<Link href={href} className="group">
|
||||
<span className="sr-only">{children}</span>
|
||||
<Icon className="h-5 w-5 fill-slate-700 transition group-hover:fill-slate-900 dark:group-hover:fill-slate-500" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const SmallPrint = () => {
|
||||
function SmallPrint() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
@@ -112,13 +112,13 @@ const SmallPrint = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Footer = () => {
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
|
||||
<PageNavigation />
|
||||
<SmallPrint />
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useId } from "react";
|
||||
|
||||
export const GridPattern = ({
|
||||
export function GridPattern({
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
@@ -13,7 +13,7 @@ export const GridPattern = ({
|
||||
x: string | number;
|
||||
y: string | number;
|
||||
squares: Array<[x: number, y: number]>;
|
||||
}) => {
|
||||
}) {
|
||||
let patternId = useId();
|
||||
|
||||
return (
|
||||
@@ -40,4 +40,4 @@ export const GridPattern = ({
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const guides = [
|
||||
},
|
||||
];
|
||||
|
||||
export const Guides = () => {
|
||||
export function Guides() {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<Heading level={2} id="guides">
|
||||
@@ -45,4 +45,4 @@ export const Guides = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Button } from "./Button";
|
||||
import { MobileNavigation, useIsInsideMobileNavigation, useMobileNavigationStore } from "./MobileNavigation";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
const TopLevelNavItem = ({ href, children }: { href: string; children: React.ReactNode }) => {
|
||||
function TopLevelNavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
@@ -21,9 +21,12 @@ const TopLevelNavItem = ({ href, children }: { href: string; children: React.Rea
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Header = forwardRef<React.ElementRef<"div">, { className?: string }>(({ className }, ref) => {
|
||||
export const Header = forwardRef<React.ElementRef<"div">, { className?: string }>(function Header(
|
||||
{ className },
|
||||
ref
|
||||
) {
|
||||
let { isOpen: mobileNavIsOpen } = useMobileNavigationStore();
|
||||
let isInsideMobileNavigation = useIsInsideMobileNavigation();
|
||||
|
||||
@@ -88,5 +91,3 @@ export const Header = forwardRef<React.ElementRef<"div">, { className?: string }
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
Header.displayName = "Header";
|
||||
|
||||
@@ -7,15 +7,15 @@ import { useInView } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const AnchorIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function AnchorIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" strokeLinecap="round" aria-hidden="true" {...props}>
|
||||
<path d="m6.5 11.5-.964-.964a3.535 3.535 0 1 1 5-5l.964.964m2 2 .964.964a3.536 3.536 0 0 1-5 5L8.5 13.5m0-5 3 3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const Eyebrow = ({ tag, label }: { tag?: string; label?: string }) => {
|
||||
function Eyebrow({ tag, label }: { tag?: string; label?: string }) {
|
||||
if (!tag && !label) {
|
||||
return null;
|
||||
}
|
||||
@@ -27,9 +27,9 @@ const Eyebrow = ({ tag, label }: { tag?: string; label?: string }) => {
|
||||
{label && <span className="font-mono text-xs text-zinc-400">{label}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const Anchor = ({ id, inView, children }: { id: string; inView: boolean; children: React.ReactNode }) => {
|
||||
function Anchor({ id, inView, children }: { id: string; inView: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<Link href={`#${id}`} className="group text-inherit no-underline hover:text-inherit">
|
||||
{inView && (
|
||||
@@ -42,9 +42,9 @@ const Anchor = ({ id, inView, children }: { id: string; inView: boolean; childre
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Heading = <Level extends 2 | 3>({
|
||||
export function Heading<Level extends 2 | 3>({
|
||||
children,
|
||||
tag,
|
||||
label,
|
||||
@@ -57,7 +57,7 @@ export const Heading = <Level extends 2 | 3>({
|
||||
label?: string;
|
||||
level?: Level;
|
||||
anchor?: boolean;
|
||||
}) => {
|
||||
}) {
|
||||
level = level ?? (2 as Level);
|
||||
let Component = `h${level}` as "h2" | "h3";
|
||||
let ref = useRef<HTMLHeadingElement>(null);
|
||||
@@ -88,4 +88,4 @@ export const Heading = <Level extends 2 | 3>({
|
||||
</Component>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
export const HeroPattern = () => {
|
||||
export function HeroPattern() {
|
||||
return (
|
||||
<div className="absolute inset-0 -z-10 mx-0 max-w-none overflow-hidden">
|
||||
<div className="absolute left-1/2 top-0 ml-[-38rem] h-[25rem] w-[81.25rem] dark:[mask-image:linear-gradient(white,transparent)]">
|
||||
@@ -28,4 +28,4 @@ export const HeroPattern = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import { Footer } from "./Footer";
|
||||
import { Header } from "./Header";
|
||||
import { type Section, SectionProvider } from "./SectionProvider";
|
||||
|
||||
export const Layout = ({
|
||||
export function Layout({
|
||||
children,
|
||||
allSections,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
allSections: Record<string, Array<Section>>;
|
||||
}) => {
|
||||
}) {
|
||||
let pathname = usePathname();
|
||||
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ export const Layout = ({
|
||||
</Link>
|
||||
</div>
|
||||
<Header />
|
||||
<Navigation className="hidden lg:mt-10 lg:block" isMobile={false} />
|
||||
<Navigation className="hidden lg:mt-10 lg:block" />
|
||||
</div>
|
||||
</motion.header>
|
||||
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
|
||||
@@ -42,4 +42,4 @@ export const Layout = ({
|
||||
</div>
|
||||
</SectionProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import logoDark from "@/images/logo/logo-dark.svg";
|
||||
import logoLight from "@/images/logo/logo-light.svg";
|
||||
import Image from "next/image";
|
||||
|
||||
export const Logo = ({ className }: { className?: string }) => {
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="block dark:hidden">
|
||||
@@ -13,4 +13,4 @@ export const Logo = ({ className }: { className?: string }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image, { ImageProps } from "next/image";
|
||||
import React from "react";
|
||||
|
||||
export const MdxImage = (props: ImageProps) => {
|
||||
export function MdxImage(props: ImageProps) {
|
||||
return <Image {...props} alt={props.alt} />;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@ import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { Fragment, Suspense, createContext, useContext, useEffect, useRef } from "react";
|
||||
import { create } from "zustand";
|
||||
|
||||
const MenuIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function MenuIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 10 9" fill="none" strokeLinecap="round" aria-hidden="true" {...props}>
|
||||
<path d="M.5 1h9M.5 8h9M.5 4.5h9" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const XIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function XIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 10 9" fill="none" strokeLinecap="round" aria-hidden="true" {...props}>
|
||||
<path d="m1.5 1 7 7M8.5 1l-7 7" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const IsInsideMobileNavigationContext = createContext(false);
|
||||
|
||||
const MobileNavigationDialog = ({ isOpen, close }: { isOpen: boolean; close: () => void }) => {
|
||||
function MobileNavigationDialog({ isOpen, close }: { isOpen: boolean; close: () => void }) {
|
||||
let pathname = usePathname();
|
||||
let searchParams = useSearchParams();
|
||||
let initialPathname = useRef(pathname).current;
|
||||
@@ -38,7 +38,7 @@ const MobileNavigationDialog = ({ isOpen, close }: { isOpen: boolean; close: ()
|
||||
}
|
||||
}, [pathname, searchParams, close, initialPathname, initialSearchParams]);
|
||||
|
||||
const onClickDialog = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
function onClickDialog(event: React.MouseEvent<HTMLDivElement>) {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const MobileNavigationDialog = ({ isOpen, close }: { isOpen: boolean; close: ()
|
||||
) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
@@ -90,18 +90,18 @@ const MobileNavigationDialog = ({ isOpen, close }: { isOpen: boolean; close: ()
|
||||
<motion.div
|
||||
layoutScroll
|
||||
className="ring-zinc-900/7.5 fixed bottom-0 left-0 top-14 w-full overflow-y-auto bg-white px-4 pb-4 pt-6 shadow-lg shadow-zinc-900/10 ring-1 min-[416px]:max-w-sm sm:px-6 sm:pb-10 dark:bg-zinc-900 dark:ring-zinc-800">
|
||||
<Navigation isMobile={true} />
|
||||
<Navigation />
|
||||
</motion.div>
|
||||
</Transition.Child>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const useIsInsideMobileNavigation = () => {
|
||||
export function useIsInsideMobileNavigation() {
|
||||
return useContext(IsInsideMobileNavigationContext);
|
||||
};
|
||||
}
|
||||
|
||||
export const useMobileNavigationStore = create<{
|
||||
isOpen: boolean;
|
||||
@@ -115,7 +115,7 @@ export const useMobileNavigationStore = create<{
|
||||
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
|
||||
}));
|
||||
|
||||
export const MobileNavigation = () => {
|
||||
export function MobileNavigation() {
|
||||
let isInsideMobileNavigation = useIsInsideMobileNavigation();
|
||||
let { isOpen, toggle, close } = useMobileNavigationStore();
|
||||
let ToggleIcon = isOpen ? XIcon : MenuIcon;
|
||||
@@ -136,4 +136,4 @@ export const MobileNavigation = () => {
|
||||
)}
|
||||
</IsInsideMobileNavigationContext.Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,55 +35,39 @@ export interface NavGroup {
|
||||
links: Array<LinkWithHref | LinkWithChildren>;
|
||||
}
|
||||
|
||||
const useInitialValue = <T,>(value: T, condition = true) => {
|
||||
function useInitialValue<T>(value: T, condition = true) {
|
||||
let initialValue = useRef(value).current;
|
||||
return condition ? initialValue : value;
|
||||
};
|
||||
}
|
||||
|
||||
const NavLink = ({
|
||||
function NavLink({
|
||||
href,
|
||||
children,
|
||||
active = false,
|
||||
isAnchorLink = false,
|
||||
}: {
|
||||
href?: string;
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
active: boolean;
|
||||
isAnchorLink?: boolean;
|
||||
}) => {
|
||||
const commonClasses = clsx(
|
||||
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
|
||||
isAnchorLink ? "pl-7" : "pl-4",
|
||||
active
|
||||
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={clsx(
|
||||
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
|
||||
isAnchorLink ? "pl-7" : "pl-4",
|
||||
active
|
||||
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
)}>
|
||||
<span className="flex w-full truncate">{children}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={clsx(
|
||||
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
|
||||
isAnchorLink ? "pl-7" : "pl-4",
|
||||
active
|
||||
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
)}>
|
||||
<span className="flex w-full truncate">{children}</span>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div aria-current={active ? "page" : undefined} className={commonClasses}>
|
||||
<span className="flex w-full truncate">{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const VisibleSectionHighlight = ({ group, pathname }: { group: NavGroup; pathname: string }) => {
|
||||
function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathname: string }) {
|
||||
let [sections, visibleSections] = useInitialValue(
|
||||
[useSectionStore((s) => s.sections), useSectionStore((s) => s.visibleSections)],
|
||||
useIsInsideMobileNavigation()
|
||||
@@ -115,9 +99,9 @@ const VisibleSectionHighlight = ({ group, pathname }: { group: NavGroup; pathnam
|
||||
style={{ height, top }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const ActivePageMarker = ({ group, pathname }: { group: NavGroup; pathname: string }) => {
|
||||
function ActivePageMarker({ group, pathname }: { group: NavGroup; pathname: string }) {
|
||||
let itemHeight = remToPx(2);
|
||||
let offset = remToPx(0.25);
|
||||
let activePageIndex = group.links.findIndex(
|
||||
@@ -138,16 +122,15 @@ const ActivePageMarker = ({ group, pathname }: { group: NavGroup; pathname: stri
|
||||
style={{ top }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const NavigationGroup = ({
|
||||
function NavigationGroup({
|
||||
group,
|
||||
className,
|
||||
activeGroup,
|
||||
setActiveGroup,
|
||||
openGroups,
|
||||
setOpenGroups,
|
||||
isMobile,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
className?: string;
|
||||
@@ -155,8 +138,7 @@ const NavigationGroup = ({
|
||||
setActiveGroup: (group: NavGroup | null) => void;
|
||||
openGroups: string[];
|
||||
setOpenGroups: (groups: string[]) => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
}) {
|
||||
const isInsideMobileNavigation = useIsInsideMobileNavigation();
|
||||
const pathname = usePathname();
|
||||
const isActiveGroup = activeGroup?.title === group.title;
|
||||
@@ -189,15 +171,13 @@ const NavigationGroup = ({
|
||||
{group.links.map((link) => (
|
||||
<motion.li key={link.title} layout="position" className="relative">
|
||||
{link.href ? (
|
||||
<NavLink
|
||||
href={isMobile && link.children ? "" : link.href}
|
||||
active={!!pathname?.startsWith(link.href)}>
|
||||
<NavLink href={link.href} active={!!pathname?.startsWith(link.href)}>
|
||||
{link.title}
|
||||
</NavLink>
|
||||
) : (
|
||||
<div onClick={() => toggleParentTitle(link.title)}>
|
||||
<NavLink
|
||||
href={!isMobile ? link.children?.[0]?.href || "" : undefined}
|
||||
href={link.children?.[0]?.href || ""}
|
||||
active={
|
||||
!!(
|
||||
isParentOpen(link.title) &&
|
||||
@@ -217,7 +197,7 @@ const NavigationGroup = ({
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{isActiveGroup && link.children && isParentOpen(link.title) && (
|
||||
{link.children && isParentOpen(link.title) && (
|
||||
<motion.ul
|
||||
role="list"
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -239,13 +219,9 @@ const NavigationGroup = ({
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavigationProps extends React.ComponentPropsWithoutRef<"nav"> {
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
|
||||
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {
|
||||
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
|
||||
const [openGroups, setOpenGroups] = useState<string[]>([]);
|
||||
|
||||
@@ -261,7 +237,6 @@ export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
|
||||
setActiveGroup={setActiveGroup}
|
||||
openGroups={openGroups}
|
||||
setOpenGroups={setOpenGroups}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
))}
|
||||
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
|
||||
@@ -276,4 +251,4 @@ export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
export const Prose = <T extends React.ElementType = "div">({
|
||||
export function Prose<T extends React.ElementType = "div">({
|
||||
as,
|
||||
className,
|
||||
...props
|
||||
}: Omit<React.ComponentPropsWithoutRef<T>, "as" | "className"> & {
|
||||
as?: T;
|
||||
className?: string;
|
||||
}) => {
|
||||
}) {
|
||||
let Component = as ?? "div";
|
||||
|
||||
return (
|
||||
@@ -21,4 +21,4 @@ export const Prose = <T extends React.ElementType = "div">({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,22 +72,22 @@ const resources: Array<Resource> = [
|
||||
},
|
||||
];
|
||||
|
||||
const ResourceIcon = ({ icon: Icon }: { icon: Resource["icon"] }) => {
|
||||
function ResourceIcon({ icon: Icon }: { icon: Resource["icon"] }) {
|
||||
return (
|
||||
<div className="dark:bg-white/7.5 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-900/5 ring-1 ring-zinc-900/25 backdrop-blur-[2px] transition duration-300 group-hover:bg-white/50 group-hover:ring-zinc-900/25 dark:ring-white/15 dark:group-hover:bg-teal-300/10 dark:group-hover:ring-teal-400">
|
||||
<Icon className="h-5 w-5 fill-zinc-700/10 stroke-zinc-700 transition-colors duration-300 group-hover:stroke-zinc-900 dark:fill-white/10 dark:stroke-zinc-400 dark:group-hover:fill-teal-300/10 dark:group-hover:stroke-teal-400" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const ResourcePattern = ({
|
||||
function ResourcePattern({
|
||||
mouseX,
|
||||
mouseY,
|
||||
...gridProps
|
||||
}: Resource["pattern"] & {
|
||||
mouseX: MotionValue<number>;
|
||||
mouseY: MotionValue<number>;
|
||||
}) => {
|
||||
}) {
|
||||
let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`;
|
||||
let style = { maskImage, WebkitMaskImage: maskImage };
|
||||
|
||||
@@ -119,17 +119,17 @@ const ResourcePattern = ({
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const Resource = ({ resource }: { resource: Resource }) => {
|
||||
function Resource({ resource }: { resource: Resource }) {
|
||||
let mouseX = useMotionValue(0);
|
||||
let mouseY = useMotionValue(0);
|
||||
|
||||
const onMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent<HTMLDivElement>) => {
|
||||
function onMouseMove({ currentTarget, clientX, clientY }: React.MouseEvent<HTMLDivElement>) {
|
||||
let { left, top } = currentTarget.getBoundingClientRect();
|
||||
mouseX.set(clientX - left);
|
||||
mouseY.set(clientY - top);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -150,9 +150,9 @@ const Resource = ({ resource }: { resource: Resource }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Resources = () => {
|
||||
export function Resources() {
|
||||
return (
|
||||
<div className="my-16 xl:max-w-none">
|
||||
<Heading level={2} id="resources">
|
||||
@@ -165,4 +165,4 @@ export const Resources = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ResponsiveVideo.js
|
||||
export const ResponsiveVideo = ({ src, title }) => {
|
||||
export function ResponsiveVideo({ src, title }) {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden pt-[56.25%]">
|
||||
<iframe
|
||||
@@ -12,4 +12,4 @@ export const ResponsiveVideo = ({ src, title }) => {
|
||||
allowFullScreen></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,19 +16,19 @@ interface HitProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Hit = ({ hit, children }: HitProps) => {
|
||||
function Hit({ hit, children }: HitProps) {
|
||||
return <Link href={hit.url}>{children}</Link>;
|
||||
};
|
||||
}
|
||||
|
||||
const SearchIcon = (props: any) => {
|
||||
function SearchIcon(props: any) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
|
||||
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Search = () => {
|
||||
export function Search() {
|
||||
let { resolvedTheme } = useTheme();
|
||||
let isLightMode = resolvedTheme === "light";
|
||||
|
||||
@@ -132,4 +132,4 @@ export const Search = () => {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ interface SectionState {
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const createSectionStore = (sections: Array<Section>) => {
|
||||
function createSectionStore(sections: Array<Section>) {
|
||||
return createStore<SectionState>()((set) => ({
|
||||
sections,
|
||||
visibleSections: [],
|
||||
@@ -49,14 +49,14 @@ const createSectionStore = (sections: Array<Section>) => {
|
||||
};
|
||||
}),
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
const useVisibleSections = (sectionStore: StoreApi<SectionState>) => {
|
||||
function useVisibleSections(sectionStore: StoreApi<SectionState>) {
|
||||
let setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections);
|
||||
let sections = useStore(sectionStore, (s) => s.sections);
|
||||
|
||||
useEffect(() => {
|
||||
const checkVisibleSections = () => {
|
||||
function checkVisibleSections() {
|
||||
let { innerHeight, scrollY } = window;
|
||||
let newVisibleSections: string[] = [];
|
||||
|
||||
@@ -90,7 +90,7 @@ const useVisibleSections = (sectionStore: StoreApi<SectionState>) => {
|
||||
}
|
||||
|
||||
setVisibleSections(newVisibleSections);
|
||||
};
|
||||
}
|
||||
|
||||
let raf = window.requestAnimationFrame(() => checkVisibleSections());
|
||||
window.addEventListener("scroll", checkVisibleSections, { passive: true });
|
||||
@@ -102,19 +102,19 @@ const useVisibleSections = (sectionStore: StoreApi<SectionState>) => {
|
||||
window.removeEventListener("resize", checkVisibleSections);
|
||||
};
|
||||
}, [setVisibleSections, sections]);
|
||||
};
|
||||
}
|
||||
|
||||
const SectionStoreContext = createContext<StoreApi<SectionState> | null>(null);
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
||||
|
||||
export const SectionProvider = ({
|
||||
export function SectionProvider({
|
||||
sections,
|
||||
children,
|
||||
}: {
|
||||
sections: Array<Section>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
}) {
|
||||
let [sectionStore] = useState(() => createSectionStore(sections));
|
||||
|
||||
useVisibleSections(sectionStore);
|
||||
@@ -124,9 +124,9 @@ export const SectionProvider = ({
|
||||
}, [sectionStore, sections]);
|
||||
|
||||
return <SectionStoreContext.Provider value={sectionStore}>{children}</SectionStoreContext.Provider>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useSectionStore = <T,>(selector: (state: SectionState) => T) => {
|
||||
const store = useContext(SectionStoreContext);
|
||||
export function useSectionStore<T>(selector: (state: SectionState) => T) {
|
||||
let store = useContext(SectionStoreContext);
|
||||
return useStore(store!, selector);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const valueColorMap = {
|
||||
DELETE: "rose",
|
||||
} as Record<string, keyof typeof colorStyles>;
|
||||
|
||||
export const Tag = ({
|
||||
export function Tag({
|
||||
children,
|
||||
variant = "medium",
|
||||
color = valueColorMap[children] ?? "teal",
|
||||
@@ -47,7 +47,7 @@ export const Tag = ({
|
||||
children: keyof typeof valueColorMap & (string | {});
|
||||
variant?: keyof typeof variantStyles;
|
||||
color?: keyof typeof colorStyles;
|
||||
}) => {
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
@@ -58,4 +58,4 @@ export const Tag = ({
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const TellaVideo = ({ tellaVideoIdentifier }: { tellaVideoIdentifier: string }) => {
|
||||
export function TellaVideo({ tellaVideoIdentifier }: { tellaVideoIdentifier: string }) {
|
||||
return (
|
||||
<div>
|
||||
<iframe
|
||||
@@ -15,4 +15,4 @@ export const TellaVideo = ({ tellaVideoIdentifier }: { tellaVideoIdentifier: str
|
||||
title="Tella Video Help"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const SunIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function SunIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
||||
<path d="M12.5 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z" />
|
||||
@@ -11,17 +11,17 @@ const SunIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const MoonIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function MoonIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
||||
<path d="M15.224 11.724a5.5 5.5 0 0 1-6.949-6.949 5.5 5.5 0 1 0 6.949 6.949Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
export function ThemeToggle() {
|
||||
let { resolvedTheme, setTheme } = useTheme();
|
||||
let otherTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
let [mounted, setMounted] = useState(false);
|
||||
@@ -40,4 +40,4 @@ export const ThemeToggle = () => {
|
||||
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const BellIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function BellIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const BellIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const BoltIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function BoltIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -8,4 +8,4 @@ export const BoltIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const BookIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function BookIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -10,4 +10,4 @@ export const BookIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m17.5 2.5-7.5 3v12l7.5-3v-12Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CalendarIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function CalendarIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -15,4 +15,4 @@ export const CalendarIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="M5.5 5.5v-3M14.5 5.5v-3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CartIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function CartIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -12,4 +12,4 @@ export const CartIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ChatBubbleIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ChatBubbleIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const ChatBubbleIcon = (props: React.ComponentPropsWithoutRef<"svg">) =>
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="M7.5 8.5h5M8.5 11.5h3" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const CheckIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function CheckIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 1.5a8.5 8.5 0 1 1 0 17 8.5 8.5 0 0 1 0-17Z" />
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="m7.5 10.5 2 2c1-3.5 3-5 3-5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ChevronRightLeftIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ChevronRightLeftIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const ChevronRightLeftIcon = (props: React.ComponentPropsWithoutRef<"svg"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ClipboardIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ClipboardIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const ClipboardIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CogIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function CogIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -16,4 +16,4 @@ export const CogIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<circle cx="10" cy="10" r="2.5" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CopyIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function CopyIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const CopyIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const DocumentIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function DocumentIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const DocumentIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="m11.5 2.5 5 5" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const EnvelopeIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function EnvelopeIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const EnvelopeIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const FaceSmileIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function FaceSmileIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 1.5a8.5 8.5 0 1 1 0 17 8.5 8.5 0 0 1 0-17Z" />
|
||||
@@ -10,4 +10,4 @@ export const FaceSmileIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const FolderIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function FolderIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -16,4 +16,4 @@ export const FolderIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const LinkIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function LinkIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const LinkIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ListIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ListIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const ListIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="M6.5 6.5h7M6.5 13.5h7M6.5 10h7" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const MagnifyingGlassIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function MagnifyingGlassIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path strokeWidth="0" d="M2.5 8.5a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z" />
|
||||
@@ -10,4 +10,4 @@ export const MagnifyingGlassIcon = (props: React.ComponentPropsWithoutRef<"svg">
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const MapPinIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function MapPinIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -16,4 +16,4 @@ export const MapPinIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<circle cx="10" cy="8" r="1.5" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PackageIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function PackageIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path strokeWidth="0" d="m10 9.5-7.5-4v9l7.5 4v-9ZM10 9.5l7.5-4v9l-7.5 4v-9Z" />
|
||||
@@ -10,4 +10,4 @@ export const PackageIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PaperAirplaneIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function PaperAirplaneIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -10,4 +10,4 @@ export const PaperAirplaneIcon = (props: React.ComponentPropsWithoutRef<"svg">)
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 19L8 12L17 3L11 19Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const PaperClipIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function PaperClipIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const PaperClipIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ShapesIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ShapesIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -14,4 +14,4 @@ export const ShapesIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const ShirtIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function ShirtIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -8,4 +8,4 @@ export const ShirtIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const SquaresPlusIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function SquaresPlusIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -9,4 +9,4 @@ export const SquaresPlusIcon = (props: React.ComponentPropsWithoutRef<"svg">) =>
|
||||
<path fill="none" strokeLinecap="round" strokeLinejoin="round" d="M14.5 11.5v6M17.5 14.5h-6" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const TagIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function TagIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -16,4 +16,4 @@ export const TagIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<circle cx="7" cy="7" r="1.5" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const UserIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function UserIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -21,4 +21,4 @@ export const UserIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const UsersIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
export function UsersIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
||||
<path
|
||||
@@ -21,4 +21,4 @@ export const UsersIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 2a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const a = Link;
|
||||
export { Button } from "@/components/Button";
|
||||
export { CodeGroup, Code as code, Pre as pre } from "@/components/Code";
|
||||
|
||||
export const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
export function wrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<article className="flex h-full flex-col pb-10 pt-16">
|
||||
<Prose className="flex-auto font-normal">{children}</Prose>
|
||||
@@ -17,13 +17,13 @@ export const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
</footer>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const h2 = (props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, "level">) => {
|
||||
export const h2 = function H2(props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, "level">) {
|
||||
return <Heading level={2} {...props} />;
|
||||
};
|
||||
|
||||
const InfoIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
function InfoIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||
<circle cx="8" cy="8" r="8" strokeWidth="0" />
|
||||
@@ -37,34 +37,34 @@ const InfoIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
|
||||
<circle cx="8" cy="4" r=".5" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Note = ({ children }: { children: React.ReactNode }) => {
|
||||
export function Note({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="my-6 flex gap-2.5 rounded-2xl border border-teal-500/20 bg-teal-50/50 p-4 leading-6 text-teal-900 dark:border-teal-500/30 dark:bg-teal-500/5 dark:text-teal-200 dark:[--tw-prose-links-hover:theme(colors.teal.300)] dark:[--tw-prose-links:theme(colors.white)]">
|
||||
<InfoIcon className="mt-1 h-4 w-4 flex-none fill-teal-500 stroke-white dark:fill-teal-200/20 dark:stroke-teal-200" />
|
||||
<div className="[&>:first-child]:mt-0 [&>:last-child]:mb-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Row = ({ children }: { children: React.ReactNode }) => {
|
||||
export function Row({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Col = ({ children, sticky = false }: { children: React.ReactNode; sticky?: boolean }) => {
|
||||
export function Col({ children, sticky = false }: { children: React.ReactNode; sticky?: boolean }) {
|
||||
return (
|
||||
<div className={clsx("[&>:first-child]:mt-0 [&>:last-child]:mb-0", sticky && "xl:sticky xl:top-24")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Properties = ({ children }: { children: React.ReactNode }) => {
|
||||
export function Properties({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="my-6">
|
||||
<ul
|
||||
@@ -74,9 +74,9 @@ export const Properties = ({ children }: { children: React.ReactNode }) => {
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Property = ({
|
||||
export function Property({
|
||||
name,
|
||||
children,
|
||||
type,
|
||||
@@ -84,7 +84,7 @@ export const Property = ({
|
||||
name: string;
|
||||
children: React.ReactNode;
|
||||
type?: string;
|
||||
}) => {
|
||||
}) {
|
||||
return (
|
||||
<li className="m-0 px-0 py-4 first:pt-0 last:pb-0">
|
||||
<dl className="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
@@ -103,4 +103,4 @@ export const Property = ({
|
||||
</dl>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const remToPx = (remValue: number) => {
|
||||
export function remToPx(remValue: number) {
|
||||
let rootFontSize =
|
||||
typeof window === "undefined"
|
||||
? 16
|
||||
: parseFloat(window.getComputedStyle(document.documentElement).fontSize);
|
||||
|
||||
return remValue * rootFontSize;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as mdxComponents from "@/components/mdx";
|
||||
import { type MDXComponents } from "mdx/types";
|
||||
|
||||
export const useMDXComponents = (components: MDXComponents) => {
|
||||
export function useMDXComponents(components: MDXComponents) {
|
||||
return {
|
||||
...components,
|
||||
...mdxComponents,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+63
-53
@@ -1,35 +1,42 @@
|
||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||
import * as acorn from "acorn";
|
||||
import { toString } from "mdast-util-to-string";
|
||||
import { mdxAnnotations } from "mdx-annotations";
|
||||
import { getHighlighter, renderToHtml } from "shiki";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { slugifyWithCounter } from '@sindresorhus/slugify'
|
||||
import * as acorn from 'acorn'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import { mdxAnnotations } from 'mdx-annotations'
|
||||
import { getHighlighter, renderToHtml } from 'shiki'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
const rehypeParseCodeBlocks = () => {
|
||||
function rehypeParseCodeBlocks() {
|
||||
return (tree) => {
|
||||
visit(tree, "element", (node, _nodeIndex, parentNode) => {
|
||||
if (node.tagName === "code" && node.properties.className) {
|
||||
parentNode.properties.language = node.properties.className[0]?.replace(/^language-/, "");
|
||||
visit(tree, 'element', (node, _nodeIndex, parentNode) => {
|
||||
if (node.tagName === 'code' && node.properties.className) {
|
||||
parentNode.properties.language = node.properties.className[0]?.replace(
|
||||
/^language-/,
|
||||
'',
|
||||
)
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let highlighter;
|
||||
let highlighter
|
||||
|
||||
const rehypeShiki = () => {
|
||||
function rehypeShiki() {
|
||||
return async (tree) => {
|
||||
highlighter = highlighter ?? (await getHighlighter({ theme: "css-variables" }));
|
||||
highlighter =
|
||||
highlighter ?? (await getHighlighter({ theme: 'css-variables' }))
|
||||
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
|
||||
let codeNode = node.children[0];
|
||||
let textNode = codeNode.children[0];
|
||||
visit(tree, 'element', (node) => {
|
||||
if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') {
|
||||
let codeNode = node.children[0]
|
||||
let textNode = codeNode.children[0]
|
||||
|
||||
node.properties.code = textNode.value;
|
||||
node.properties.code = textNode.value
|
||||
|
||||
if (node.properties.language) {
|
||||
let tokens = highlighter.codeToThemedTokens(textNode.value, node.properties.language);
|
||||
let tokens = highlighter.codeToThemedTokens(
|
||||
textNode.value,
|
||||
node.properties.language,
|
||||
)
|
||||
|
||||
textNode.value = renderToHtml(tokens, {
|
||||
elements: {
|
||||
@@ -37,68 +44,71 @@ const rehypeShiki = () => {
|
||||
code: ({ children }) => children,
|
||||
line: ({ children }) => `<span>${children}</span>`,
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rehypeSlugify = () => {
|
||||
function rehypeSlugify() {
|
||||
return (tree) => {
|
||||
let slugify = slugifyWithCounter();
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName === "h2" && !node.properties.id) {
|
||||
node.properties.id = slugify(toString(node));
|
||||
let slugify = slugifyWithCounter()
|
||||
visit(tree, 'element', (node) => {
|
||||
if (node.tagName === 'h2' && !node.properties.id) {
|
||||
node.properties.id = slugify(toString(node))
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rehypeAddMDXExports = (getExports) => {
|
||||
function rehypeAddMDXExports(getExports) {
|
||||
return (tree) => {
|
||||
let exports = Object.entries(getExports(tree));
|
||||
let exports = Object.entries(getExports(tree))
|
||||
|
||||
for (let [name, value] of exports) {
|
||||
for (let node of tree.children) {
|
||||
if (node.type === "mdxjsEsm" && new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)) {
|
||||
return;
|
||||
if (
|
||||
node.type === 'mdxjsEsm' &&
|
||||
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let exportStr = `export const ${name} = ${value}`;
|
||||
let exportStr = `export const ${name} = ${value}`
|
||||
|
||||
tree.children.push({
|
||||
type: "mdxjsEsm",
|
||||
type: 'mdxjsEsm',
|
||||
value: exportStr,
|
||||
data: {
|
||||
estree: acorn.parse(exportStr, {
|
||||
sourceType: "module",
|
||||
ecmaVersion: "latest",
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}),
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const getSections = (node) => {
|
||||
let sections = [];
|
||||
function getSections(node) {
|
||||
let sections = []
|
||||
|
||||
for (let child of node.children ?? []) {
|
||||
if (child.type === "element" && child.tagName === "h2") {
|
||||
if (child.type === 'element' && child.tagName === 'h2') {
|
||||
sections.push(`{
|
||||
title: ${JSON.stringify(toString(child))},
|
||||
id: ${JSON.stringify(child.properties.id)},
|
||||
...${child.properties.annotation}
|
||||
}`);
|
||||
}`)
|
||||
} else if (child.children) {
|
||||
sections.push(...getSections(child));
|
||||
sections.push(...getSections(child))
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
return sections
|
||||
}
|
||||
|
||||
export const rehypePlugins = [
|
||||
mdxAnnotations.rehype,
|
||||
@@ -111,4 +121,4 @@ export const rehypePlugins = [
|
||||
sections: `[${getSections(tree).join()}]`,
|
||||
}),
|
||||
],
|
||||
];
|
||||
]
|
||||
|
||||
+60
-59
@@ -1,36 +1,51 @@
|
||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||
import glob from "fast-glob";
|
||||
import * as fs from "fs";
|
||||
import { toString } from "mdast-util-to-string";
|
||||
import * as path from "path";
|
||||
import { remark } from "remark";
|
||||
import remarkMdx from "remark-mdx";
|
||||
import { createLoader } from "simple-functional-loader";
|
||||
import { filter } from "unist-util-filter";
|
||||
import { SKIP, visit } from "unist-util-visit";
|
||||
import * as url from "url";
|
||||
import { slugifyWithCounter } from '@sindresorhus/slugify'
|
||||
import glob from 'fast-glob'
|
||||
import * as fs from 'fs'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import * as path from 'path'
|
||||
import { remark } from 'remark'
|
||||
import remarkMdx from 'remark-mdx'
|
||||
import { createLoader } from 'simple-functional-loader'
|
||||
import { filter } from 'unist-util-filter'
|
||||
import { SKIP, visit } from 'unist-util-visit'
|
||||
import * as url from 'url'
|
||||
|
||||
const extractSections = () => {
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const processor = remark().use(remarkMdx).use(extractSections)
|
||||
const slugify = slugifyWithCounter()
|
||||
|
||||
function isObjectExpression(node) {
|
||||
return (
|
||||
node.type === 'mdxTextExpression' &&
|
||||
node.data?.estree?.body?.[0]?.expression?.type === 'ObjectExpression'
|
||||
)
|
||||
}
|
||||
|
||||
function excludeObjectExpressions(tree) {
|
||||
return filter(tree, (node) => !isObjectExpression(node))
|
||||
}
|
||||
|
||||
function extractSections() {
|
||||
return (tree, { sections }) => {
|
||||
slugify.reset();
|
||||
slugify.reset()
|
||||
|
||||
visit(tree, (node) => {
|
||||
if (node.type === "heading" || node.type === "paragraph") {
|
||||
let content = toString(excludeObjectExpressions(node));
|
||||
if (node.type === "heading" && node.depth <= 2) {
|
||||
let hash = node.depth === 1 ? null : slugify(content);
|
||||
sections.push([content, hash, []]);
|
||||
if (node.type === 'heading' || node.type === 'paragraph') {
|
||||
let content = toString(excludeObjectExpressions(node))
|
||||
if (node.type === 'heading' && node.depth <= 2) {
|
||||
let hash = node.depth === 1 ? null : slugify(content)
|
||||
sections.push([content, hash, []])
|
||||
} else {
|
||||
sections.at(-1)?.[2].push(content);
|
||||
sections.at(-1)?.[2].push(content)
|
||||
}
|
||||
return SKIP;
|
||||
return SKIP
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const Search = (nextConfig = {}) => {
|
||||
let cache = new Map();
|
||||
export default function Search(nextConfig = {}) {
|
||||
let cache = new Map()
|
||||
|
||||
return Object.assign({}, nextConfig, {
|
||||
webpack(config, options) {
|
||||
@@ -38,26 +53,26 @@ export const Search = (nextConfig = {}) => {
|
||||
test: __filename,
|
||||
use: [
|
||||
createLoader(function () {
|
||||
let appDir = path.resolve("./src/app");
|
||||
this.addContextDependency(appDir);
|
||||
let appDir = path.resolve('./src/app')
|
||||
this.addContextDependency(appDir)
|
||||
|
||||
let files = glob.sync("**/*.mdx", { cwd: appDir });
|
||||
let files = glob.sync('**/*.mdx', { cwd: appDir })
|
||||
let data = files.map((file) => {
|
||||
let url = "/" + file.replace(/(^|\/)page\.mdx$/, "");
|
||||
let mdx = fs.readFileSync(path.join(appDir, file), "utf8");
|
||||
let url = '/' + file.replace(/(^|\/)page\.mdx$/, '')
|
||||
let mdx = fs.readFileSync(path.join(appDir, file), 'utf8')
|
||||
|
||||
let sections = [];
|
||||
let sections = []
|
||||
|
||||
if (cache.get(file)?.[0] === mdx) {
|
||||
sections = cache.get(file)[1];
|
||||
sections = cache.get(file)[1]
|
||||
} else {
|
||||
let vfile = { value: mdx, sections };
|
||||
processor.runSync(processor.parse(vfile), vfile);
|
||||
cache.set(file, [mdx, sections]);
|
||||
let vfile = { value: mdx, sections }
|
||||
processor.runSync(processor.parse(vfile), vfile)
|
||||
cache.set(file, [mdx, sections])
|
||||
}
|
||||
|
||||
return { url, sections };
|
||||
});
|
||||
return { url, sections }
|
||||
})
|
||||
|
||||
// When this file is imported within the application
|
||||
// the following module is loaded:
|
||||
@@ -91,7 +106,7 @@ export const Search = (nextConfig = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const search = (query, options = {}) => {
|
||||
export function search(query, options = {}) {
|
||||
let result = sectionIndex.search(query, {
|
||||
...options,
|
||||
enrich: true,
|
||||
@@ -105,30 +120,16 @@ export const Search = (nextConfig = {}) => {
|
||||
pageTitle: item.doc.pageTitle,
|
||||
}))
|
||||
}
|
||||
`;
|
||||
`
|
||||
}),
|
||||
],
|
||||
});
|
||||
})
|
||||
|
||||
if (typeof nextConfig.webpack === "function") {
|
||||
return nextConfig.webpack(config, options);
|
||||
if (typeof nextConfig.webpack === 'function') {
|
||||
return nextConfig.webpack(config, options)
|
||||
}
|
||||
|
||||
return config;
|
||||
return config
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const processor = remark().use(remarkMdx).use(extractSections);
|
||||
const slugify = slugifyWithCounter();
|
||||
|
||||
const isObjectExpression = (node) => {
|
||||
return (
|
||||
node.type === "mdxTextExpression" && node.data?.estree?.body?.[0]?.expression?.type === "ObjectExpression"
|
||||
);
|
||||
};
|
||||
|
||||
const excludeObjectExpressions = (tree) => {
|
||||
return filter(tree, (node) => !isObjectExpression(node));
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import nextMDX from "@next/mdx";
|
||||
import { recmaPlugins } from "./mdx/recma.mjs";
|
||||
import { rehypePlugins } from "./mdx/rehype.mjs";
|
||||
import { remarkPlugins } from "./mdx/remark.mjs";
|
||||
import { Search as withSearch } from "./mdx/search.mjs";
|
||||
import withSearch from "./mdx/search.mjs";
|
||||
|
||||
const withMDX = nextMDX({
|
||||
options: {
|
||||
|
||||
+2
-4
@@ -1,12 +1,10 @@
|
||||
import { MetadataRoute } from "next";
|
||||
|
||||
const robots = (): MetadataRoute.Robots => {
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default robots;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -7,5 +7,5 @@ declare module "@/mdx/search.mjs" {
|
||||
pageTitle?: string;
|
||||
};
|
||||
|
||||
export const search: (query: string, options?: SearchOptions) => Array<Result>;
|
||||
export function search(query: string, options?: SearchOptions): Array<Result>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type PluginUtils } from "tailwindcss/types/config";
|
||||
|
||||
const typographyStyles = ({ theme }: PluginUtils) => {
|
||||
export default function typographyStyles({ theme }: PluginUtils) {
|
||||
return {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
@@ -367,6 +367,4 @@ const typographyStyles = ({ theme }: PluginUtils) => {
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default typographyStyles;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import { dirname, join } from "path";
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
const getAbsolutePath = (value: string): any => {
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
};
|
||||
|
||||
export const config: StorybookConfig = {
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../../../packages/ui/**/stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
@@ -25,3 +24,4 @@ export const config: StorybookConfig = {
|
||||
autodocs: "tag",
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Preview } from "@storybook/react";
|
||||
|
||||
import "../src/index.css";
|
||||
|
||||
export const preview: Preview = {
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
@@ -13,3 +13,5 @@ export const preview: Preview = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
export const App = () => {
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
@@ -23,4 +23,6 @@ export const App = () => {
|
||||
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
+4
-3
@@ -18,11 +18,12 @@ RUN turbo prune @formbricks/web --docker
|
||||
FROM base AS installer
|
||||
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
|
||||
|
||||
|
||||
# Set hardcoded environment variables
|
||||
ENV DATABASE_URL="postgresql://placeholder:for@build:5432/gets_overwritten_at_runtime?schema=public"
|
||||
ENV NEXTAUTH_SECRET="placeholder_for_next_auth_of_64_chars_get_overwritten_at_runtime"
|
||||
@@ -58,7 +59,7 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
|
||||
## step 3: setup production runner
|
||||
#
|
||||
FROM base AS runner
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& apk add --no-cache supercronic \
|
||||
@@ -91,5 +92,5 @@ RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
(cd packages/database && npm run db:migrate:deploy) && \
|
||||
(cd packages/database && pnpm db:migrate:deploy) && \
|
||||
exec node apps/web/server.js
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import FormbricksClient from "@/app/(app)/components/FormbricksClient";
|
||||
import PosthogIdentify from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -10,9 +10,9 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||
import ToasterClient from "@formbricks/ui/ToasterClient";
|
||||
|
||||
const EnvLayout = async ({ children, params }) => {
|
||||
export default async function EnvLayout({ children, params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
@@ -54,6 +54,4 @@ const EnvLayout = async ({ children, params }) => {
|
||||
</ResponseFilterProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvLayout;
|
||||
}
|
||||
|
||||
+10
-10
@@ -30,11 +30,11 @@ import { TProduct } from "@formbricks/types/product";
|
||||
import { TBaseFilters, TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const surveyMutateAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
export async function surveyMutateAction(survey: TSurvey): Promise<TSurvey> {
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
}
|
||||
|
||||
export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
export async function updateSurveyAction(survey: TSurvey): Promise<TSurvey> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
@@ -45,7 +45,7 @@ export const updateSurveyAction = async (survey: TSurvey): Promise<TSurvey> => {
|
||||
if (!hasCreateOrUpdateAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateSurvey(survey);
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -190,7 +190,7 @@ export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
|
||||
return await resetSegmentInSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const getImagesFromUnsplashAction = async (searchQuery: string, page: number = 1) => {
|
||||
export async function getImagesFromUnsplashAction(searchQuery: string, page: number = 1) {
|
||||
if (!UNSPLASH_ACCESS_KEY) {
|
||||
throw new Error("Unsplash access key is not set");
|
||||
}
|
||||
@@ -231,9 +231,9 @@ export const getImagesFromUnsplashAction = async (searchQuery: string, page: num
|
||||
} catch (error) {
|
||||
throw new Error("Error getting images from Unsplash");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) => {
|
||||
export async function triggerDownloadUnsplashImageAction(downloadUrl: string) {
|
||||
try {
|
||||
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
method: "GET",
|
||||
@@ -249,9 +249,9 @@ export const triggerDownloadUnsplashImageAction = async (downloadUrl: string) =>
|
||||
} catch (error) {
|
||||
throw new Error("Error downloading image from Unsplash");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const createActionClassAction = async (action: TActionClassInput) => {
|
||||
export async function createActionClassAction(action: TActionClassInput) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
@@ -259,4 +259,4 @@ export const createActionClassAction = async (action: TActionClassInput) => {
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createActionClass(action.environmentId, action);
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ interface AddQuestionButtonProps {
|
||||
product: TProduct;
|
||||
}
|
||||
|
||||
export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonProps) => {
|
||||
export default function AddQuestionButton({ addQuestion, product }: AddQuestionButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -59,4 +59,4 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { LogicEditor } from "./LogicEditor";
|
||||
import { UpdateQuestionId } from "./UpdateQuestionId";
|
||||
import LogicEditor from "./LogicEditor";
|
||||
import UpdateQuestionId from "./UpdateQuestionId";
|
||||
|
||||
interface AdvancedSettingsProps {
|
||||
question: TSurveyQuestion;
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { TSurveyBackgroundBgType, TSurveyStyling } from "@formbricks/types/surve
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
|
||||
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
|
||||
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
|
||||
|
||||
interface BackgroundStylingCardProps {
|
||||
open: boolean;
|
||||
@@ -75,7 +75,7 @@ export const BackgroundStylingCard = ({
|
||||
asChild
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full cursor-pointer rounded-lg hover:bg-slate-50",
|
||||
"h-full w-full cursor-pointer rounded-lg hover:bg-slate-50",
|
||||
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
|
||||
)}>
|
||||
<div className="inline-flex px-4 py-4">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user