Merge branch 'master' into fix/table-editor-link

This commit is contained in:
Ivan Vasilov committed 2025-07-29 11:23:35 +02:00
commit 5dfefe84b9
6111 files changed
+556399 -313584

No files matched your search

@@ -0,0 +1,59 @@
# Documentation Embeddings Generation System
## Overview
The documentation embeddings generation system processes various documentation sources and uploads their metadata to a database for semantic search functionality. The system is located in `apps/docs/scripts/search/` and works by:
1. **Discovering content sources** from multiple types of documentation
2. **Processing content** into structured sections with checksums
3. **Generating embeddings** using OpenAI's text-embedding-ada-002 model
4. **Storing in database** with vector embeddings for semantic search
## Architecture
### Main Entry Point
- `generate-embeddings.ts` - Main script that orchestrates the entire process
- Supports `--refresh` flag to force regeneration of all content
### Content Sources (`sources/` directory)
#### Base Classes
- `BaseLoader` - Abstract class for loading content from different sources
- `BaseSource` - Abstract class for processing and formatting content
#### Source Types
1. **Markdown Sources** (`markdown.ts`)
- Processes `.mdx` files from guides and documentation
- Extracts frontmatter metadata and content sections
2. **Reference Documentation** (`reference-doc.ts`)
- **OpenAPI References** - Management API documentation from OpenAPI specs
- **Client Library References** - JavaScript, Dart, Python, C#, Swift, Kotlin SDKs
- **CLI References** - Command-line interface documentation
- Processes YAML/JSON specs and matches with common sections
3. **GitHub Discussions** (`github-discussion.ts`)
- Fetches troubleshooting discussions from GitHub using GraphQL API
- Uses GitHub App authentication for access
4. **Partner Integrations** (`partner-integrations.ts`)
- Fetches approved partner integration documentation from Supabase database
- Technology integrations only (excludes agencies)
### Processing Flow
1. **Content Discovery**: Each source loader discovers and loads content files/data
2. **Content Processing**: Each source processes content into:
- Checksum for change detection
- Metadata (title, subtitle, etc.)
- Sections with headings and content
3. **Change Detection**: Compares checksums against existing database records
4. **Embedding Generation**: Uses OpenAI to generate embeddings for new/changed content
5. **Database Storage**: Stores in `page` and `page_section` tables with embeddings
6. **Cleanup**: Removes outdated pages using version tracking
### Database Schema
- **`page`** table: Stores page metadata, content, checksum, version
- **`page_section`** table: Stores individual sections with embeddings, token counts
+267
View File
@@ -0,0 +1,267 @@
---
description: Docs GraphQL Architecture
globs: apps/docs/resources/**/*.ts
alwaysApply: false
---
# Docs GraphQL Architecture
## Overview
The `/apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
## Architecture Pattern
Each GraphQL query follows this structure:
```
resources/
├── queryObject/
│ ├── queryObjectModel.ts # Data models and business logic
│ ├── queryObjectSchema.ts # GraphQL type definitions
│ ├── queryObjectResolver.ts # Query resolver and arguments
│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
├── utils/
│ ├── connections.ts # GraphQL connection/pagination utilities
│ └── fields.ts # GraphQL field selection utilities
├── rootSchema.ts # Main GraphQL schema with all queries
└── rootSync.ts # Root sync script for syncing to database
```
## Example queries
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
2. **error** (`error/`) - Error code lookup for Supabase services
3. **schema** - GraphQL schema introspection
## Key Files
### `rootSchema.ts`
- Main GraphQL schema definition
- Imports all resolvers and combines them into the root query
- Defines the `RootQueryType` with all top-level fields
### `utils/connections.ts`
- Provides `createCollectionType()` for paginated collections
- `GraphQLCollectionBuilder` for building collection responses
- Standard pagination arguments and edge/node patterns
### `utils/fields.ts`
- `graphQLFields()` utility to analyze requested fields in resolvers
- Used for optimizing data fetching based on what fields are actually requested
## Creating a New Top-Level Query
To add a new GraphQL query, follow these steps:
### 1. Create Query Folder Structure
```bash
mkdir resources/newQuery
touch resources/newQuery/newQueryModel.ts
touch resources/newQuery/newQuerySchema.ts
touch resources/newQuery/newQueryResolver.ts
```
### 2. Define GraphQL Schema (`newQuerySchema.ts`)
```typescript
import { GraphQLObjectType, GraphQLString } from 'graphql'
export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
name: 'NewQuery',
description: 'Description of what this query returns',
fields: {
id: {
type: GraphQLString,
description: 'Unique identifier',
},
// Add other fields...
},
})
```
### 3. Create Data Model (`newQueryModel.ts`)
> [!NOTE]
> The data model should be agnostic to GraphQL. It may import argument types
> from `~/__generated__/graphql`, but otherwise all functions and classes
> should be unaware of whether they are called for GraphQL resolution.
> [!TIP]
> The types in `~/__generated__/graphql` for a new endpoint will not exist
> until the code generation is run in the next step.
```typescript
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
import { Result } from '~/features/helpers.fn'
import { supabase } from '~/lib/supabase'
export class NewQueryModel {
constructor(public readonly data: {
id: string
// other properties...
}) {}
static async loadData(
args: RootQueryTypeNewQueryArgs,
requestedFields: Array<string>
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
// Implement data fetching logic
const result = new Result(
await supabase()
.from('your_table')
.select('*')
// Add filters based on args
)
.map((data) => data.map((item) => new NewQueryModel(item)))
.mapError(convertPostgrestToApiError)
return result
}
}
```
### 4. Create Resolver (`newQueryResolver.ts`)
```typescript
import { GraphQLError, GraphQLNonNull, GraphQLString, type GraphQLResolveInfo } from 'graphql'
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
import { convertUnknownToApiError } from '~/app/api/utils'
import { Result } from '~/features/helpers.fn'
import { graphQLFields } from '../utils/fields'
import { NewQueryModel } from './newQueryModel'
import { GRAPHQL_FIELD_NEW_QUERY, GraphQLObjectTypeNewQuery } from './newQuerySchema'
async function resolveNewQuery(
_parent: unknown,
args: RootQueryTypeNewQueryArgs,
_context: unknown,
info: GraphQLResolveInfo
): Promise<NewQueryModel[] | GraphQLError> {
return (
await Result.tryCatchFlat(
resolveNewQueryImpl,
convertUnknownToApiError,
args,
info
)
).match(
(data) => data,
(error) => {
console.error(`Error resolving ${GRAPHQL_FIELD_NEW_QUERY}:`, error)
return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
}
)
}
async function resolveNewQueryImpl(
args: RootQueryTypeNewQueryArgs,
info: GraphQLResolveInfo
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
const fieldsInfo = graphQLFields(info)
const requestedFields = Object.keys(fieldsInfo)
return await NewQueryModel.loadData(args, requestedFields)
}
export const newQueryRoot = {
[GRAPHQL_FIELD_NEW_QUERY]: {
description: 'Description of what this query does',
args: {
id: {
type: new GraphQLNonNull(GraphQLString),
description: 'Required argument description',
},
// Add other arguments...
},
type: GraphQLObjectTypeNewQuery, // or createCollectionType() for lists
resolve: resolveNewQuery,
},
}
```
### 5. Register in Root Schema
In `rootSchema.ts`, add your resolver:
```typescript
// Import your resolver
import { newQueryRoot } from './newQuery/newQueryResolver'
// Add to the query fields
export const rootGraphQLSchema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
...introspectRoot,
...searchRoot,
...errorRoot,
...newQueryRoot, // Add this line
},
}),
types: [
GraphQLObjectTypeGuide,
GraphQLObjectTypeReferenceCLICommand,
GraphQLObjectTypeReferenceSDKFunction,
GraphQLObjectTypeTroubleshooting,
],
})
```
### 6. Update TypeScript Types
Run the GraphQL codegen to update TypeScript types:
```bash
pnpm run -F docs codegen:graphql
```
## Best Practices
1. **Error Handling**: Error handling always uses the Result class, defined in apps/docs/features/helpers.fn.ts
2. **Field Optimization**: Use `graphQLFields()` to only fetch requested data
3. **Collections**: Use `createCollectionType()` for paginated lists
4. **Naming**: Use `GRAPHQL_FIELD_*` constants for field names
5. **Documentation**: Add GraphQL descriptions to all fields and types
6. **Database**: Use `supabase()` client for database operations with `convertPostgrestToApiError`
## Testing
Tests are located in apps/docs/app/api/graphql/tests. Each top-level query
should have its own test file, located at <queryName>.test.ts.
### Test data
Test data uses a local database, seeded with the file at supabase/seed.sql. Add
any data required for running your new query.
### Integration tests
Integration tests import the POST function defined in
apps/docs/api/graphql/route.ts, then make a request to this function.
For example:
```ts
import { POST } from '../route'
it('test name', async () => {
const query = `
query {
...
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query }),
})
const result = await POST(request)
})
```
Include at least the following tests:
1. A test that requests all fields (including nested fields) on the new query
object, and asserts that there are no errors, and the requested fields are
properly returned.
2. A test that triggers and error, and asserts that a GraphQL error is properly
returned.
+71
View File
@@ -0,0 +1,71 @@
---
description: Docs Testing Procedure
globs: apps/docs/**/*.test.ts
alwaysApply: false
---
# Docs Test Requirements
Rules for running tests in the docs application, ensuring proper Supabase setup and test execution.
<rule>
name: docs_test_requirements
description: Standards for running tests in the docs application with proper Supabase setup
filters:
# Match test files in the docs app
- type: file_extension
pattern: "\\.(test|spec)\\.(ts|tsx)$"
- type: path
pattern: "^apps/docs/.*"
# Match test execution events
- type: event
pattern: "test_execution"
actions:
- type: suggest
message: |
Before running tests in the docs app:
1. Check Supabase status:
```bash
pnpm supabase status
```
2. If Supabase is not running:
```bash
pnpm supabase start
```
3. Reset the database to ensure clean state:
```bash
pnpm supabase db reset --local
```
4. Run the tests:
```bash
pnpm run -F docs test:local:unwatch
```
Important notes:
- Always ensure Supabase is running before tests
- Database must be reset to ensure clean state
- Use test:local:unwatch to run tests without watch mode
- Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx}
examples:
- input: |
# Bad: Running tests without proper setup
pnpm run -F docs test
pnpm run -F docs test:local
# Good: Proper test execution sequence
pnpm supabase status
pnpm supabase start # if not running
pnpm supabase db reset --local
pnpm run -F docs test:local:unwatch
output: "Correctly executed docs tests with proper Supabase setup"
metadata:
priority: high
version: 1.0
</rule>
@@ -0,0 +1,6 @@
---
description:
globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx
alwaysApply: false
---
Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md)
+14 -3
View File
@@ -1,10 +1,21 @@
/packages/shared-data/pricing.ts @roryw10 @kevcodez
/packages/shared-data/plans.ts @roryw10 @kevcodez
/packages/ui/ @supabase/design
/packages/shared-data/pricing.ts @roryw10 @supabase/billing
/packages/shared-data/plans.ts @roryw10 @supabase/billing
/packages/common/telemetry-constants.ts @supabase/growth-eng
/apps/studio/ @supabase/Dashboard
# temporary until we've ironed out all the problems with sync workflow
/apps/docs/content/troubleshooting/ @charislam
/apps/www/ @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
/docker/ @supabase/cli
/docker/ @supabase/dev-workflows
/apps/studio/csp.js @supabase/security
/apps/studio/components/interfaces/Billing/Payment @supabase/security
/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
+11 -4
View File
@@ -17,6 +17,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
@@ -33,14 +36,18 @@ jobs:
with:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Type check
run: npm run typecheck
run: pnpm run typecheck
- name: Run tests
run: npm run test
run: pnpm run test
+28 -1
View File
@@ -42,4 +42,31 @@ jobs:
run: |
echo "Applying triage label for new issue"
gh issue edit "$NUMBER" --add-label "$LABELS"
spam-detection:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check GitHub Issue for spam
env:
POST_URL: ${{ secrets.POST_URL }}
BEARER_TOKEN: ${{ secrets.BEARER_TOKEN }}
NUMBER: ${{ github.event.issue.number }}
run: |
RESPONSE=$(curl -s -X POST "$POST_URL" \
-H "Authorization: Bearer $BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"issue_id": $NUMBER}')
echo "spam_response=$RESPONSE" >> $GITHUB_OUTPUT
- name: Use spam detector output to label issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABEL: flagged
run: |
IS_SPAM=$(echo "$SPAM_RESPONSE" | jq -r '.spam')
if [ "$IS_SPAM" == "true" ]; then
echo "Applying flagged label for new issue suspected of spam"
gh issue edit "$NUMBER" --add-label "$LABEL"
fi
+24 -18
View File
@@ -5,44 +5,50 @@ on:
types:
- labeled
permissions:
contents: write
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
autofix:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
steps:
- name: Calculate number of commits
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
- uses: actions/checkout@v4
with:
sparse-checkout: apps
ref: ${{ github.head_ref }}
token: ${{ secrets.PAT_AUTOFIX }}
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
sparse-checkout: |
packages
apps
- uses: pnpm/action-setup@v4
name: Install pnpm
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: pnpm
- name: Install required dependencies
run: npm ci
run: pnpm i
- name: Run Prettier in fix mode
run: npm run format
run: pnpm run format
- name: Commit changes and push to existing branch
run: |
git config --global user.name 'github-tidy-bot'
git config --global user.email 'github-tidy-bot@supabase.com'
if [[ `git status --porcelain` ]]; then
echo "[bot] Changes detected, committing."
echo "[bot] Running in non-squash mode."
git commit -am "ci: Autofix updates from GitHub workflow"
git push
else
echo "[bot] No changes detected, nothing to commit."
fi
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "ci: Autofix updates from GitHub workflow"
commit_user_name: "kevcodez"
commit_user_email: "k.grueneberg1994@gmail.com"
+4 -1
View File
@@ -8,6 +8,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
misspell:
name: runner / misspell
@@ -25,7 +28,7 @@ jobs:
exclude: |
*.css
**/package.json
**/package-lock.json
**/pnpm-lock.yaml
./.git/*
*.ipynb
./i18n/README.*.md
-63
View File
@@ -1,63 +0,0 @@
name: Config (Preview)
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- master
paths:
- "supabase/**"
workflow_dispatch:
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
apply:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
TF_VAR_git_branch: ${{ github.head_ref }}
TF_CLI_ARGS_apply: -target=supabase_settings.preview
defaults:
run:
working-directory: supabase/remotes
outputs:
db_user: ${{ steps.branch.outputs.user }}
db_pass: ${{ steps.branch.outputs.password }}
db_host: ${{ steps.branch.outputs.host }}
db_port: ${{ steps.branch.outputs.port }}
jwt_secret: ${{ steps.branch.outputs.jwt_secret }}
ref: ${{ steps.branch.outputs.id }}
status: ${{ steps.branch.outputs.status }}
version: ${{ steps.branch.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
- id: branch
run: |
terraform output -json branch_database \
| jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \
>> "$GITHUB_OUTPUT"
-28
View File
@@ -1,28 +0,0 @@
name: Config (Production)
on:
push:
branches:
- master
paths:
- "supabase/remotes/**"
workflow_dispatch:
jobs:
apply:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
defaults:
run:
working-directory: supabase/remotes
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
+12 -4
View File
@@ -10,13 +10,16 @@ on:
required: false
type: boolean
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- name: Check out repo
@@ -26,20 +29,25 @@ jobs:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
run: pnpm i
- name: Update last-changed dates
working-directory: ./apps/docs
if: ${{ !inputs.reset }}
run: npm run last-changed
run: pnpm run last-changed
- name: Reset last-changed dates
working-directory: ./apps/docs
if: ${{ inputs.reset }}
run: npm run last-changed:reset
run: pnpm run last-changed:reset
@@ -0,0 +1,67 @@
name: docs_lint_comment_external
# This is a continuation of ./docs-lint-v2.yml, to write comments on external
# PRs.
#
# SECURITY:
# This workflow runs with write permissions, in the context of code from an
# external PR. This is safe because no external code is executed. The
# stringified Markdown output from the linter (downloaded as an artifact) is
# directly written as the body of a PR comment.
on:
workflow_run:
workflows: [docs_lint]
types:
- completed
permissions:
pull-requests: write
jobs:
comment_on_pr:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
steps:
- id: download_artifact
name: 'Download artifact'
uses: actions/github-script@v7
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }}
});
const matchingArtifact = artifacts?.data?.artifacts?.find(
(artifact) => artifact.name == 'lint_results'
);
if (matchingArtifact) {
core.setOutput('contains_results', 'true')
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchingArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{ github.workspace }}/lint_results.zip', Buffer.from(download.data));
}
- id: unzip_results
name: Unzip results file
if: steps.download_artifact.outputs.contains_results == 'true'
run: unzip lint_results.zip
- name: 'Comment on PR'
if: steps.download_artifact.outputs.contains_results == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prNumber = Number(fs.readFileSync('./pr_number.txt'));
const lintResults = fs.readFileSync('./lint_results.txt', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: lintResults
});
@@ -0,0 +1,61 @@
name: '[Docs] Lint v2 (scheduled)'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
permissions:
contents: write
pull-requests: write
jobs:
lint-all:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- name: cache cargo
id: cache-cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: 3186b58a532c98d7f470f2b887c2b74a086d5f2e
- name: install linter
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 3186b58a532c98d7f470f2b887c2b74a086d5f2e
- name: run linter
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
supa-mdx-lint apps/docs/content || {
echo "Linter failed, attempting to fix errors..."
git config --global user.name 'github-docs-bot'
git config --global user.email 'github-docs-bot@supabase.com'
BRANCH_NAME="bot/docs-lint-fixes"
EXISTING_BRANCH=$(git ls-remote --heads origin $BRANCH_NAME)
if [[ -n "$EXISTING_BRANCH" ]]; then
git push origin --delete $BRANCH_NAME
fi
git checkout -b $BRANCH_NAME
supa-mdx-lint apps/docs/content --fix || FIX_FAILED=1
git add .
git commit -m '[bot] fix lint errors' || true
git push origin $BRANCH_NAME
gh pr create --title '[bot] fix lint errors' --body 'This PR fixes lint errors in the documentation.' --head $BRANCH_NAME
if [ "${FIX_FAILED:-0}" -eq 1 ]; then
echo "Fix did not correct all errors."
exit 1
fi
}
+107
View File
@@ -0,0 +1,107 @@
name: docs_lint
# Runs the docs linter on PRs that edit docs content.
# There are two branches of this workflow for internal and external PRs, due
# to the security design of GitHub Actions.
#
# Internal PRs:
# Have write permissions, so comments are written directly by reviewdog.
#
# External PRs:
# Have read-only permissions, so lint results are uploaded as an artifact, to
# be written to the PR in a subsequent workflow_run action that has write
# permissions. See ./docs/lint-v2-comment.yml.
#
# See https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
on:
pull_request:
env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
permissions:
pull-requests: write
jobs:
supa-mdx-lint:
name: supa-mdx-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout: |
supa-mdx-lint.config.toml
supa-mdx-lint
apps/docs/content
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 #v3.0.2
id: filter
with:
filters: |
docs:
- 'apps/docs/content/**'
- 'supa-mdx-lint/**'
- 'supa-mdx-lint.config.toml'
- name: cache cargo
id: cache-cargo
if: steps.filter.outputs.docs == 'true'
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: 3186b58a532c98d7f470f2b887c2b74a086d5f2e
- name: install linter
if: steps.filter.outputs.docs == 'true' && steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 3186b58a532c98d7f470f2b887c2b74a086d5f2e
- name: install reviewdog
if: steps.filter.outputs.docs == 'true'
uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0
with:
reviewdog_version: v0.20.2
- name: run linter (internal)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name == github.repository
env:
BASE_REF: ${{ github.base_ref }}
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
git diff --name-only "origin/$BASE_REF" HEAD \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -r supa-mdx-lint --format rdf \
| reviewdog -f=rdjsonl -reporter=github-pr-review -tee
- id: external_lint
name: run linter (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository
env:
BASE_REF: ${{ github.base_ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -o pipefail
run_lints() {
git diff --name-only "origin/$BASE_REF" HEAD \
| { grep -E "^apps/docs/content/" || test $? = 1; } \
| xargs -rx -n 1000000000 supa-mdx-lint --format markdown
}
set +e
LINT_RESULTS=$(run_lints)
LINT_EXIT_CODE=$?
set -e
echo "LINT_EXIT_CODE=$LINT_EXIT_CODE" >> $GITHUB_OUTPUT
if [[ $LINT_EXIT_CODE -ne 0 ]]; then
mkdir -p ./__github_actions__pr
echo "${{ github.event.number }}" > ./__github_actions__pr/pr_number.txt
echo "$LINT_RESULTS" > ./__github_actions__pr/lint_results.txt
fi
- name: save results as artifact (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
uses: actions/upload-artifact@v4
with:
name: lint_results
path: __github_actions__pr/
- name: fail if linter fails (external)
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
run: exit 1
+33 -24
View File
@@ -6,36 +6,45 @@ on:
- cron: '0 0 * * 1'
workflow_dispatch:
permissions:
pull-requests: write
contents: write
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
apps/docs
- uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
apps/docs
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install deps
run: npm ci
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Install deps
run: pnpm i
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
@@ -0,0 +1,56 @@
name: '[Docs] Sync troubleshooting guides to GitHub Discussions'
on:
push:
branches:
- master
paths:
- 'apps/docs/content/troubleshooting/**'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-troubleshooting:
runs-on: ubuntu-latest
env:
DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run Troubleshooting script
run: pnpm run -F docs troubleshooting:sync
- name: Create Pull Request
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with:
commit-message: '[bot] sync troubleshooting guides to db'
title: '[bot] sync troubleshooting guides to db'
author: 'github-docs-sync-bot <github-docs-sync-bot@supabase.com>'
branch: 'bot/docs-sync-troubleshooting'
branch-suffix: 'random'
+49
View File
@@ -0,0 +1,49 @@
# All syncs from the repo to the DB will eventually be consolidated in a single
# script run by this workflow.
name: docs_sync
on:
push:
branches:
- master
paths:
# Resync if the content changes
- 'apps/docs/content/**'
# Resync if the resource definition or sync scripts change
- 'apps/docs/resources/**'
workflow_dispatch:
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/docs
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run sync script
run: pnpm run -F docs sync
+11 -3
View File
@@ -11,6 +11,9 @@ concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -22,14 +25,19 @@ jobs:
apps/docs
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm --prefix="apps/docs" run test:smoke
run: pnpm --prefix="apps/docs" run test:smoke
+18 -3
View File
@@ -11,6 +11,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -22,15 +25,27 @@ jobs:
apps/docs
examples
packages
supabase
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:docs
run: |
touch .env
# Needed to prevent local Supabase startup from erroring, due to
# GitHub Auth being enabled in config
echo "GITHUB_CLIENT_ID=dummy-id" >> .env
echo "GITHUB_SECRET=dummy-secret" >> .env
pnpm run test:docs
+31
View File
@@ -0,0 +1,31 @@
name: Comment on external PRs
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
comment-on-external-pr:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const commentBody = `
Thanks for contributing to Supabase! ❤️ Our team will review your PR.
A few tips for a smoother review process:
- If you have a local version of the repo, run \`pnpm run format\` to make sure formatting checks pass.
- Once we've reviewed your PR, please don't trivially merge master (don't click \`Update branch\` if there are no merge conflicts to be fixed). This invalidates any pre-merge checks we've run.
`;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
+4
View File
@@ -9,6 +9,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
build:
runs-on: ubuntu-latest
-25
View File
@@ -1,25 +0,0 @@
name: reviewdog
on:
pull_request:
paths:
- '.vale.ini'
- 'vale/**'
- 'apps/docs/content/**'
jobs:
vale:
name: runner / vale
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
sparse-checkout: |
.vale.ini
vale
apps/docs/content
- uses: errata-ai/vale-action@reviewdog
with:
files: apps/docs/content
# [Charis] Temporary because we still have to go through giant backlog of existing errors
# Don't want people to worry about this (just want them looking at the specific PR comments)
# fail_on_error: true
+4 -1
View File
@@ -8,6 +8,9 @@ on:
- 'supabase/functions/og-images/**'
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
@@ -18,7 +21,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup the Supabase CLI
uses: supabase/setup-cli@v1
+54
View File
@@ -0,0 +1,54 @@
name: PG Meta Tests
on:
push:
branches: ['master']
paths:
- 'packages/pg-meta/**/*'
pull_request:
branches: ['master']
paths:
- 'packages/pg-meta/**/*'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
packages/pg-meta
packages/tsconfig
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
- name: Run tests
run: pnpm --filter=@supabase/pg-meta run test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
directory: packages/pg-meta/coverage
flags: pg-meta
-45
View File
@@ -1,45 +0,0 @@
name: Playwright Tests
on:
push:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
pull_request:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: supabase/setup-cli@v1
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:playwright
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
# continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-tests/playwright-report/
retention-days: 7
+28 -13
View File
@@ -10,6 +10,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
format:
runs-on: ubuntu-latest
@@ -18,17 +21,21 @@ jobs:
uses: actions/checkout@v4
with:
sparse-checkout: apps
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
cache: 'pnpm'
- name: Download dependencies
run: |
npm ci
pnpm i
- name: Run prettier
run: |-
npm run test:prettier
pnpm run test:prettier
# i18n is not a node package, so we handle that one separately
format-i18n:
@@ -39,19 +46,21 @@ jobs:
with:
sparse-checkout: |
i18n
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
cache: 'pnpm'
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
pnpm i
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
pnpm exec prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
format-sql:
runs-on: ubuntu-latest
@@ -62,13 +71,19 @@ jobs:
sparse-checkout: |
apps/docs/pages
apps/docs/content
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
pnpm i
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs npx prettier -c
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs pnpm exec prettier -c
+2 -2
View File
@@ -20,8 +20,8 @@ jobs:
flavor: |
latest=true
tags: |
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'schedule' }}
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'workflow_dispatch' }}
type=sha,prefix={{date 'YYYY.MM.DD'}}-sha-,enable=${{ github.event_name == 'schedule' }}
type=sha,prefix={{date 'YYYY.MM.DD'}}-sha-,enable=${{ github.event_name == 'workflow_dispatch' }}
release_x86:
needs: settings
+22 -12
View File
@@ -17,17 +17,26 @@ on:
schedule:
- cron: '0 0 * * *'
concurrency:
group: ${{ github.workflow }}-prod
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
NEXT_PUBLIC_MISC_URL: ${{ secrets.NEXT_PUBLIC_MISC_URL}}
NEXT_PUBLIC_MISC_ANON_KEY: ${{ secrets.NEXT_PUBLIC_MISC_ANON_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- name: Check out repo
@@ -38,24 +47,25 @@ jobs:
apps/www/.env.local.example
supabase
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
# Need the miscellaneous use API, which is available publicly (by design) in www
- name: Copy environment variables
run: cp apps/www/.env.local.example apps/docs/.env
run: pnpm i
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
run: npm run embeddings
run: pnpm run embeddings
- name: Refresh embeddings
working-directory: ./apps/docs
if: ${{ inputs.refresh }}
run: npm run embeddings:refresh
run: pnpm run embeddings:refresh
@@ -0,0 +1,28 @@
name: Self-Host Tests Smoke
on:
pull_request:
branches: ['master']
paths:
- 'docker/**/*'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
docker/
- name: Run docker-compose up
# Ensure all services can be started and healthy with default config
run: cd docker && cp .env.example .env && docker compose up --wait
+5
View File
@@ -3,6 +3,11 @@ on:
schedule:
- cron: '30 1 * * *'
permissions:
issues: write
pull-requests: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
+93
View File
@@ -0,0 +1,93 @@
name: Studio E2E Tests
on:
push:
branches: [master]
paths:
- 'packages/pg-meta/**/*'
- 'apps/studio/**'
- 'e2e/studio/**'
- 'pnpm-lock.yaml'
pull_request:
paths:
- 'packages/pg-meta/**/*'
- 'apps/studio/**'
- 'e2e/studio/**'
- 'pnpm-lock.yaml'
- '.github/workflows/studio-e2e-test.yml'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
# Make the job non-blocking
continue-on-error: true
# Require approval only for external contributors
environment: ${{ github.event.pull_request.author_association != 'MEMBER' && 'Studio E2E Tests' || '' }}
env:
EMAIL: ${{ secrets.CI_EMAIL }}
PASSWORD: ${{ secrets.CI_PASSWORD }}
PROJECT_REF: ${{ secrets.CI_PROJECT_REF }}
NEXT_PUBLIC_IS_PLATFORM: true
NEXT_PUBLIC_API_URL: https://api.supabase.green
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_STUDIO_HOSTED_PROJECT_ID }}
NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Install Vercel CLI
run: pnpm add --global vercel@latest
- name: Pull Vercel Environment Information (Preview)
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts for Vercel
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project to Vercel and Get URL
id: deploy_vercel
run: |
DEPLOY_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "Vercel Preview URL: $DEPLOY_URL"
echo "DEPLOY_URL=$DEPLOY_URL" >> $GITHUB_OUTPUT
- name: Install Playwright Browsers
run: pnpm -C e2e/studio exec playwright install --with-deps
- name: Run Playwright tests
id: playwright
env:
AUTHENTICATION: true
STUDIO_URL: ${{ steps.deploy_vercel.outputs.DEPLOY_URL }}/dashboard
run: pnpm e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-artifacts
path: |
e2e/studio/playwright-report/
e2e/studio/test-results/
retention-days: 7
-47
View File
@@ -1,47 +0,0 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Studio Unit Tests & Build Check
on:
push:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'package-lock.json'
pull_request:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'package-lock.json'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check:
# Uses larger hosted runner as it significantly decreases build times
runs-on: [larger-runner-4cpu]
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/studio
packages
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
working-directory: ./
- name: Run Tests
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: '--max_old_space_size=3072'
run: npm run test:studio
working-directory: ./
+78
View File
@@ -0,0 +1,78 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Studio Unit Tests & Build Check
on:
push:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'pnpm-lock.yaml'
pull_request:
branches: [master, studio]
paths:
- 'apps/studio/**'
- 'pnpm-lock.yaml'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
# Uses larger hosted runner as it significantly decreases build times
runs-on:
group: Default Larger Runners
strategy:
matrix:
test_number: [1]
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/studio
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm i
working-directory: ./
- name: Run Tests
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: '--max_old_space_size=3072'
run: pnpm run test:ci
working-directory: ./apps/studio
- name: Upload coverage results to Coveralls
uses: coverallsapp/github-action@master
with:
parallel: true
flag-name: studio-tests
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./apps/studio/coverage/lcov.info
base-path: './apps/studio'
finish:
needs: test
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}
+14 -5
View File
@@ -10,26 +10,35 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
typecheck:
# Uses larger hosted runner as it significantly decreases build times
runs-on: [larger-runner-4cpu]
runs-on:
group: Default Larger Runners
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run TypeScript type check
run: npx turbo run typecheck
run: pnpm exec turbo run typecheck
- name: Run Lint
run: npx turbo run lint
run: pnpm exec turbo run lint
+11 -3
View File
@@ -11,6 +11,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -21,14 +24,19 @@ jobs:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:ui-patterns
run: pnpm run test:ui-patterns
+36 -4
View File
@@ -11,9 +11,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test_number: [1]
steps:
- uses: actions/checkout@v4
@@ -21,14 +27,40 @@ jobs:
sparse-checkout: |
packages
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install deps
run: npm ci
run: pnpm i
- name: Run tests
run: npm run test:ui
run: pnpm run test:ci
working-directory: ./packages/ui
- name: Upload coverage results to Coveralls
uses: coverallsapp/github-action@master
with:
parallel: true
flag-name: ui-tests
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./packages/ui/coverage/lcov.info
base-path: './packages/ui'
finish:
needs: test
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
parallel-finished: true
github-token: ${{ secrets.GITHUB_TOKEN }}
+10
View File
@@ -133,3 +133,13 @@ gcloud.json
# sitemaps
# apps/www/public/*.xml
# apps/docs/public/*.xml
# CLI version file
.temp/cli-latest
.pnpm-store/*
# Sentry CLI config
**/.sentryclirc
keys.json
+3 -1
View File
@@ -1 +1,3 @@
engine-strict=true
engine-strict=true
update-notifier=false
@jsr:registry=https://npm.jsr.io
+1 -1
View File
@@ -1 +1 @@
20
22
+17 -3
View File
@@ -7,12 +7,26 @@ apps/**/out
# prettier-plugin-sql-cst only supports sqlite syntax
**/supabase/migrations/*.sql
apps/www/schema.sql
apps/www/public/images/*
apps/docs/**/generated/*
apps/docs/examples/*
examples/slack-clone/nextjs-slack-clone/full-schema.sql
# ignore files with custom js formatting
apps/docs/pages/guides/auth/*.mdx
apps/docs/pages/guides/integrations/*.mdx
apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/design-system/__registry__
apps/design-system/__registry__
packages/icons/__registry__
packages/icons/src/icons/*.ts
apps/ui-library/__registry__
apps/ui-library/public/r
apps/**/.contentlayer
# invalid JSON file
packages/ui/src/components/Form/examples/PhoneProvidersSchema.json
apps/cms/config/api.ts
# files auto-generated by payload cms
apps/cms/src/app/*
apps/cms/src/migrations/*
apps/cms/src/payload-types.ts
# ignore because of <br/><br/>
apps/www/_blog/2025-07-14-supabase-ui-platform-kit.mdx
-1
View File
@@ -6,7 +6,6 @@
"printWidth": 100,
"endOfLine": "lf",
"sqlKeywordCase": "lower",
"pluginSearchDirs": false,
"plugins": ["prettier-plugin-sql-cst"],
"overrides": [
{
-14
View File
@@ -1,14 +0,0 @@
StylesPath = './vale/styles'
MinAlertLevel = 'suggestion'
Vocab = Supabase
[formats]
mdx = md
[*]
BasedOnStyles = Vale, Custom
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>), \
(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \
(?s) *(<Image.*?/>)
TokenIgnores = (href\s*([=:])\s*['"].*?\1), \
+17 -23
View File
@@ -12,7 +12,7 @@
- [Shared components](#shared-components)
- [Installing packages](#installing-packages)
- [Running Docker for Supabase Studio](#running-docker-for-supabase-studio)
- [Prerequsites](#prerequsites)
- [Prerequisites](#prerequisites)
- [Get Started](#get-started)
- [Create a pull request](#create-a-pull-request)
- [Issue assignment](#issue-assignment)
@@ -22,8 +22,6 @@
- [Community channels](#community-channels)
- [Contributors](#contributors)
- [Common tasks](#common-tasks)
- [Add a redirect](#add-a-redirect)
- [Community channels](#community-channels)
## Getting started
@@ -36,9 +34,9 @@ To ensure a positive and inclusive environment, please read our [code of conduct
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](http://git-scm.com/)
- [Node.js v20.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 10.x.x or higher
- [Git](https://git-scm.com/)
- [Node.js v22.x or higher](https://nodejs.org)
- [pnpm](https://pnpm.io/) version 9.x.x or higher
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
@@ -67,10 +65,10 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
### Install dependencies
1. Install the dependencies in the root of the repo.
1. Install the dependencies in the root of the repo:
```sh
npm install # install dependencies
pnpm install # install dependencies
```
2. Copy the example `.env.local.example` to `.env.local`
@@ -79,9 +77,9 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
cp apps/www/.env.local.example apps/www/.env.local
```
3. After that you can run the apps simultaneously with the following.
3. After that you can run the apps simultaneously with the following:
```sh
npm run dev # start all the applications
pnpm dev # start all the applications
```
Then visit, and edit, any of the following sites:
@@ -97,7 +95,7 @@ Then visit, and edit, any of the following sites:
You can run any of the sites individually by using the scope name. For example:
```sh
npm run dev:www
pnpm dev:www
```
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
@@ -115,17 +113,13 @@ The monorepo has a set of shared components under `/packages`:
#### Installing packages
Installing a package with NPM workspaces requires you to add the `-w` flag to tell NPM which workspace you want to install into. Do not install dependencies in their local folder, install them from the route using the `-w` flag.
The format is: `npm install <package name> -w=<workspace to install in>`.
Installing a package in a specific workspace requires you to move to the workspace and then run the install command.
For example:
- `npm install react -w common`: installs into `./packages/common`
- `npm install react -w www`: installs into `./apps/www`
- `npm install react -w studio`: installs into `./apps/studio`
1. `cd apps/studio`: move to the `studio` workspace.
2. `pnpm add react`: installs `react` into `studio` workspace.
You do not need to install `devDependencies` in each workspace. These can all be installed in the root package.
---
@@ -133,7 +127,7 @@ You do not need to install `devDependencies` in each workspace. These can all be
To run Studio locally, you'll need to setup Docker in addition to your NextJS frontend.
#### Prerequsites
#### Prerequisites
First, make sure you have the Docker installed on your device. You can download and install it from [here](https://docs.docker.com/get-docker/).
@@ -151,7 +145,7 @@ First, make sure you have the Docker installed on your device. You can download
cp .env.example .env
```
3. Run docker
3. Run docker:
```sh
docker compose up
@@ -192,9 +186,9 @@ Create a new entry in the [`redirects.js`](https://github.com/supabase/supabase/
We support "federating" docs, meaning doc content can come directly from external repos other than [`supabase/supabase`](https://github.com/supabase/supabase).
- It's great for things like client libs who have their own set of docs that we don't want to duplicate on the official Supabase docs (eg. [`supabase/vecs`](https://github.com/supabase/vecs)).
- No duplication or manual steps required - fetches and generates automatically as part of the docs build pipeline
- It's flexible - you can "embed" external docs nearly anywhere at any level in Supabase docs, but they will feel native
- If you are maintaining a repo containing docs that you think could also live in Supabase docs, feel free to create an issue and we can work together to integrate
- No duplication or manual steps required - fetches and generates automatically as part of the docs build pipeline.
- It's flexible - you can "embed" external docs nearly anywhere at any level in Supabase docs, but they will feel native.
- If you are maintaining a repo containing docs that you think could also live in Supabase docs, feel free to create an issue and we can work together to integrate.
Federated docs work using Next.js's build pipeline. We use `getStaticProps()` to fetch remote documentation (ie. markdown) at build time which is processed and passed to the respective page within the docs.
+6 -5
View File
@@ -5,7 +5,7 @@
# Supabase
[Supabase](https://supabase.com) is an open source Firebase alternative. We're building the features of Firebase using enterprise-grade open source tools.
[Supabase](https://supabase.com) is the Postgres development platform. We're building the features of Firebase using enterprise-grade open source tools.
- [x] Hosted Postgres Database. [Docs](https://supabase.com/docs/guides/database)
- [x] Authentication and Authorization. [Docs](https://supabase.com/docs/guides/auth)
@@ -52,10 +52,10 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
- [GoTrue](https://github.com/supabase/gotrue) is a JWT based API for managing users and issuing JWT tokens.
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API.
- [GoTrue](https://github.com/supabase/gotrue) is a JWT-based authentication API that simplifies user sign-ups, logins, and session management in your applications.
- [Storage](https://github.com/supabase/storage-api) a RESTful API for managing files in S3, with Postgres handling permissions.
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API.
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
@@ -244,6 +244,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
- [Bangla / বাংলা](/i18n/README.bn.md)
- [Bulgarian / Български](/i18n/README.bg.md)
- [Catalan / Català](/i18n/README.ca.md)
- [Croatian / Hrvatski](/i18n/README.hr.md)
- [Czech / čeština](/i18n/README.cs.md)
- [Danish / Dansk](/i18n/README.da.md)
- [Dutch / Nederlands](/i18n/README.nl.md)
+13
View File
@@ -0,0 +1,13 @@
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=
S3_ENDPOINT=
NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog
CRON_SECRET=YOUR_CRON_SECRET_HERE
PREVIEW_SECRET=YOUR_SECRET_HERE
BLOG_APP_URL=http://localhost:3000
File renamed without changes.
+43
View File
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
/.idea/*
!/.idea/runConfigurations
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
/media
+71
View File
@@ -0,0 +1,71 @@
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
FROM node:22.12.0-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Remove this line if you do not have this folder
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js
+23
View File
@@ -0,0 +1,23 @@
# Payload CMS
### Local Development
1. run `cd apps/cms && supabase start` to start the local supabase project
2. run `cp .env.example .env` to copy the example environment variables and update the variables. You'll need to add the `S3_` variables to your `.env` to use Supabase Storage
3. `pnpm install && pnpm generate:importmap` to install dependencies and start the dev server
4. run `pnpm dev` in the apps/cms folder or `pnpm dev:cms` from the root
5. open `http://localhost:3030` to open the app in your browser
Follow the on-screen instructions to login and create the first admin user.
### Collections
Collections are what data looks like in the Payload cms schema. The following are the collections currently configured in the app.
- Authors
- Categories
- Events
- Media
- Posts
- Tags
- Users
+30
View File
@@ -0,0 +1,30 @@
version: '3'
services:
payload:
image: node:18-alpine
ports:
- '3030:3030'
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
depends_on:
- postgres
env_file:
- .env
# Uncomment the following to use postgres
postgres:
restart: always
image: postgres:latest
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
data:
# pgdata:
node_modules:
+31
View File
@@ -0,0 +1,31 @@
import { withPayload } from '@payloadcms/next/withPayload'
import redirects from './redirects.js'
const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: undefined || process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000'
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => {
const url = new URL(item)
return {
hostname: url.hostname,
protocol: url.protocol.replace(':', ''),
}
}),
],
},
reactStrictMode: true,
redirects,
eslint: {
// We are already running linting via GH action, this will skip linting during production build on Vercel
ignoreDuringBuilds: true,
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })
+47
View File
@@ -0,0 +1,47 @@
{
"name": "cms",
"version": "1.0.0",
"description": "Payload CMS for Supabase",
"license": "MIT",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build --turbopack",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --turbopack --port 3030",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"ci": "cross-env NODE_OPTIONS=--no-deprecation payload migrate && pnpm build",
"clean": "rimraf node_modules .next",
"typecheck_IGNORED": "tsc --noEmit"
},
"dependencies": {
"@payloadcms/db-postgres": "3.33.0",
"@payloadcms/live-preview-react": "^3.33.0",
"@payloadcms/next": "3.33.0",
"@payloadcms/payload-cloud": "3.33.0",
"@payloadcms/plugin-form-builder": "3.33.0",
"@payloadcms/plugin-nested-docs": "3.33.0",
"@payloadcms/plugin-seo": "3.33.0",
"@payloadcms/richtext-lexical": "3.33.0",
"@payloadcms/storage-s3": "3.33.0",
"@payloadcms/ui": "3.33.0",
"common": "workspace:*",
"config": "workspace:*",
"cross-env": "^7.0.3",
"eslint-config-supabase": "workspace:*",
"graphql": "^16.8.1",
"next": "catalog:",
"payload": "3.33.0",
"react": "catalog:",
"react-dom": "catalog:",
"sharp": "0.32.6"
},
"devDependencies": {
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"typescript": "5.7.3"
}
}
+20
View File
@@ -0,0 +1,20 @@
const redirects = async () => {
const internetExplorerRedirect = {
destination: '/ie-incompatible.html',
has: [
{
type: 'header',
key: 'user-agent',
value: '(.*Trident.*)', // all ie browsers
},
],
permanent: false,
source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page
}
const redirects = [internetExplorerRedirect]
return redirects
}
export default redirects
+14
View File
@@ -0,0 +1,14 @@
import type { AccessArgs, FieldAccess } from 'payload'
import type { User } from '@/payload-types'
type isAdmin = (args: AccessArgs<User>) => boolean
export const isAdmin: isAdmin = ({ req: { user } }) => {
// Return true or false based on if the user has an admin role
return Boolean(user?.roles?.includes('admin'))
}
export const isAdminFieldLevel: FieldAccess<{ id: string }, User> = ({ req: { user } }) => {
// Return true or false based on if the user has an admin role
return Boolean(user?.roles?.includes('admin'))
}
+21
View File
@@ -0,0 +1,21 @@
import type { Access } from 'payload'
export const isAdminOrSelf: Access = ({ req: { user } }) => {
// Need to be logged in
if (user) {
// If user has role of 'admin'
if (user.roles?.includes('admin')) {
return true
}
// If any other type of user, only provide access to themselves
return {
id: {
equals: user.id,
},
}
}
// Reject everyone else
return false
}
+3
View File
@@ -0,0 +1,3 @@
import type { Access } from 'payload'
export const isAnyone: Access = () => true
+9
View File
@@ -0,0 +1,9 @@
import type { AccessArgs } from 'payload'
import type { User } from '@/payload-types'
type isAuthenticated = (args: AccessArgs<User>) => boolean
export const isAuthenticated: isAuthenticated = ({ req: { user } }) => {
return Boolean(user)
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react'
import './styles.css'
export const metadata = {
description: 'Content Management System for the Supabase website',
title: 'Supabase CMS',
}
export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
return (
<html lang="en">
<body>
<main>{children}</main>
</body>
</html>
)
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default async function HomePage() {
redirect('/admin')
}
+164
View File
@@ -0,0 +1,164 @@
:root {
--font-mono: 'Roboto Mono', monospace;
}
* {
box-sizing: border-box;
}
html {
font-size: 18px;
line-height: 32px;
background: rgb(0, 0, 0);
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
}
body {
font-family: system-ui;
font-size: 18px;
line-height: 32px;
margin: 0;
color: rgb(1000, 1000, 1000);
@media (max-width: 1024px) {
font-size: 15px;
line-height: 24px;
}
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
@media (max-width: 1024px) {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
@media (max-width: 768px) {
font-size: 38px;
line-height: 38px;
}
@media (max-width: 400px) {
font-size: 32px;
line-height: 32px;
}
}
p {
margin: 24px 0;
@media (max-width: 1024px) {
margin: calc(var(--base) * 0.75) 0;
}
}
a {
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}
.home {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
@media (max-width: 400px) {
padding: 24px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
h1 {
text-align: center;
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
.footer {
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
}
p {
margin: 0;
}
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
}
}
}
@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })
export default NotFound
@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
export default Page
@@ -0,0 +1,43 @@
import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent'
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
export const importMap = {
"@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24
}
@@ -0,0 +1,19 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const PUT = REST_PUT(config)
export const OPTIONS = REST_OPTIONS(config)
@@ -0,0 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)
@@ -0,0 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)
+31
View File
@@ -0,0 +1,31 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout
+14
View File
@@ -0,0 +1,14 @@
import configPromise from '@payload-config'
import { getPayload } from 'payload'
export const GET = async () => {
const payload = await getPayload({
config: configPromise,
})
const data = await payload.find({
collection: 'users',
})
return Response.json(data)
}
@@ -0,0 +1,65 @@
import type { Post, ArchiveBlock as ArchiveBlockProps } from '@/payload-types'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import RichText from '@/components/RichText'
import { CollectionArchive } from '@/components/CollectionArchive'
export const ArchiveBlock: React.FC<
ArchiveBlockProps & {
id?: string
}
> = async (props) => {
const { id, categories, introContent, limit: limitFromProps, populateBy, selectedDocs } = props
const limit = limitFromProps || 3
let posts: Post[] = []
if (populateBy === 'collection') {
const payload = await getPayload({ config: configPromise })
const flattenedCategories = categories?.map((category) => {
if (typeof category === 'object') return category.id
else return category
})
const fetchedPosts = await payload.find({
collection: 'posts',
depth: 1,
limit,
...(flattenedCategories && flattenedCategories.length > 0
? {
where: {
categories: {
in: flattenedCategories,
},
},
}
: {}),
})
posts = fetchedPosts.docs
} else {
if (selectedDocs?.length) {
const filteredSelectedPosts = selectedDocs.map((post) => {
if (typeof post.value === 'object') return post.value
}) as Post[]
posts = filteredSelectedPosts
}
}
return (
<div className="my-16" id={`block-${id}`}>
{introContent && (
<div className="container mb-16">
<RichText className="ms-0 max-w-[48rem]" data={introContent} enableGutter={false} />
</div>
)}
<CollectionArchive posts={posts} />
</div>
)
}
@@ -0,0 +1,94 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const Archive: Block = {
slug: 'archive',
interfaceName: 'ArchiveBlock',
fields: [
{
name: 'introContent',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: 'Intro Content',
},
{
name: 'populateBy',
type: 'select',
defaultValue: 'collection',
options: [
{
label: 'Collection',
value: 'collection',
},
{
label: 'Individual Selection',
value: 'selection',
},
],
},
{
name: 'relationTo',
type: 'select',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
},
defaultValue: 'posts',
label: 'Collections To Show',
options: [
{
label: 'Posts',
value: 'posts',
},
],
},
{
name: 'categories',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
},
hasMany: true,
label: 'Categories To Show',
relationTo: 'categories',
},
{
name: 'limit',
type: 'number',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'collection',
step: 1,
},
defaultValue: 10,
label: 'Limit',
},
{
name: 'selectedDocs',
type: 'relationship',
admin: {
condition: (_, siblingData) => siblingData.populateBy === 'selection',
},
hasMany: true,
label: 'Selection',
relationTo: ['posts'],
},
],
labels: {
plural: 'Archives',
singular: 'Archive',
},
}
+26
View File
@@ -0,0 +1,26 @@
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
import { cn } from '@/utilities/ui'
import React from 'react'
import RichText from '@/components/RichText'
type Props = {
className?: string
} & BannerBlockProps
export const BannerBlock: React.FC<Props> = ({ className, content, style }) => {
return (
<div className={cn('mx-auto my-8 w-full', className)}>
<div
className={cn('border py-3 px-6 flex items-center rounded', {
'border-border bg-card': style === 'info',
'border-error bg-error/30': style === 'error',
'border-success bg-success/30': style === 'success',
'border-warning bg-warning/30': style === 'warning',
})}
>
<RichText data={content} enableGutter={false} enableProse={false} />
</div>
</div>
)
}
+37
View File
@@ -0,0 +1,37 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const Banner: Block = {
slug: 'banner',
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'info',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Error', value: 'error' },
{ label: 'Success', value: 'success' },
],
required: true,
},
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [...rootFeatures, FixedToolbarFeature(), InlineToolbarFeature()]
},
}),
label: false,
required: true,
},
],
interfaceName: 'BannerBlock',
}
@@ -0,0 +1,23 @@
import React from 'react'
import type { CallToActionBlock as CTABlockProps } from '@/payload-types'
import RichText from '@/components/RichText'
import { CMSLink } from '@/components/Link'
export const CallToActionBlock: React.FC<CTABlockProps> = ({ links, richText }) => {
return (
<div className="container">
<div className="bg-card rounded border-border border p-4 flex flex-col gap-8 md:flex-row md:justify-between md:items-center">
<div className="max-w-[48rem] flex items-center">
{richText && <RichText className="mb-0" data={richText} enableGutter={false} />}
</div>
<div className="flex flex-col gap-8">
{(links || []).map(({ link }, i) => {
return <CMSLink key={i} size="lg" {...link} />
})}
</div>
</div>
</div>
)
}
@@ -0,0 +1,42 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { linkGroup } from '../../fields/linkGroup'
export const CallToAction: Block = {
slug: 'cta',
interfaceName: 'CallToActionBlock',
fields: [
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: false,
},
linkGroup({
appearances: ['default', 'outline'],
overrides: {
maxRows: 2,
},
}),
],
labels: {
plural: 'Calls to Action',
singular: 'Call to Action',
},
}
@@ -0,0 +1,33 @@
'use client'
import { Highlight, themes } from 'prism-react-renderer'
import React from 'react'
import { CopyButton } from './CopyButton'
type Props = {
code: string
language?: string
}
export const Code: React.FC<Props> = ({ code, language = '' }) => {
if (!code) return null
return (
<Highlight code={code} language={language} theme={themes.vsDark}>
{({ getLineProps, getTokenProps, tokens }) => (
<pre className="bg-black p-4 border text-xs border-border rounded overflow-x-auto">
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ className: 'table-row', line })}>
<span className="table-cell select-none text-right text-white/25">{i + 1}</span>
<span className="table-cell pl-4">
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
))}
<CopyButton code={code} />
</pre>
)}
</Highlight>
)
}
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import { Code } from './Component.client'
export type CodeBlockProps = {
code: string
language?: string
blockType: 'code'
}
type Props = CodeBlockProps & {
className?: string
}
export const CodeBlock: React.FC<Props> = ({ className, code, language }) => {
return (
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
<Code code={code} language={language} />
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
'use client'
import { Button } from '@/components/ui/button'
import { CopyIcon } from '@payloadcms/ui/icons/Copy'
import { useState } from 'react'
export function CopyButton({ code }: { code: string }) {
const [text, setText] = useState('Copy')
function updateCopyStatus() {
if (text === 'Copy') {
setText(() => 'Copied!')
setTimeout(() => {
setText(() => 'Copy')
}, 1000)
}
}
return (
<div className="flex justify-end align-middle">
<Button
className="flex gap-1"
variant={'secondary'}
onClick={async () => {
await navigator.clipboard.writeText(code)
updateCopyStatus()
}}
>
<p>{text}</p>
<CopyIcon />
</Button>
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
import type { Block } from 'payload'
export const Code: Block = {
slug: 'code',
interfaceName: 'CodeBlock',
fields: [
{
name: 'language',
type: 'select',
defaultValue: 'typescript',
options: [
{
label: 'Typescript',
value: 'typescript',
},
{
label: 'Javascript',
value: 'javascript',
},
{
label: 'CSS',
value: 'css',
},
],
},
{
name: 'code',
type: 'code',
label: false,
required: true,
},
],
}
+43
View File
@@ -0,0 +1,43 @@
import { cn } from '@/utilities/ui'
import React from 'react'
import RichText from '@/components/RichText'
import type { ContentBlock as ContentBlockProps } from '@/payload-types'
import { CMSLink } from '../../components/Link'
export const ContentBlock: React.FC<ContentBlockProps> = (props) => {
const { columns } = props
const colsSpanClasses = {
full: '12',
half: '6',
oneThird: '4',
twoThirds: '8',
}
return (
<div className="container my-16">
<div className="grid grid-cols-4 lg:grid-cols-12 gap-y-8 gap-x-16">
{columns &&
columns.length > 0 &&
columns.map((col, index) => {
const { enableLink, link, richText, size } = col
return (
<div
className={cn(`col-span-4 lg:col-span-${colsSpanClasses[size!]}`, {
'md:col-span-2': size !== 'full',
})}
key={index}
>
{richText && <RichText data={richText} enableGutter={false} />}
{enableLink && <CMSLink {...link} />}
</div>
)
})}
</div>
</div>
)
}
+79
View File
@@ -0,0 +1,79 @@
import type { Block, Field } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { link } from '@/fields/link'
const columnFields: Field[] = [
{
name: 'size',
type: 'select',
defaultValue: 'oneThird',
options: [
{
label: 'One Third',
value: 'oneThird',
},
{
label: 'Half',
value: 'half',
},
{
label: 'Two Thirds',
value: 'twoThirds',
},
{
label: 'Full',
value: 'full',
},
],
},
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: false,
},
{
name: 'enableLink',
type: 'checkbox',
},
link({
overrides: {
admin: {
condition: (_data, siblingData) => {
return Boolean(siblingData?.enableLink)
},
},
},
}),
]
export const Content: Block = {
slug: 'content',
interfaceName: 'ContentBlock',
fields: [
{
name: 'columns',
type: 'array',
admin: {
initCollapsed: true,
},
fields: columnFields,
},
],
}
@@ -0,0 +1,45 @@
import type { CheckboxField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { useFormContext } from 'react-hook-form'
import { Checkbox as CheckboxUi } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
export const Checkbox: React.FC<
CheckboxField & {
errors: Partial<FieldErrorsImpl>
register: UseFormRegister<FieldValues>
}
> = ({ name, defaultValue, errors, label, register, required, width }) => {
const props = register(name, { required: required })
const { setValue } = useFormContext()
return (
<Width width={width}>
<div className="flex items-center gap-2">
<CheckboxUi
defaultChecked={defaultValue}
id={name}
{...props}
onCheckedChange={(checked) => {
setValue(props.name, checked)
}}
/>
<Label htmlFor={name}>
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
{label}
</Label>
</div>
{errors[name] && <Error name={name} />}
</Width>
)
}
+162
View File
@@ -0,0 +1,162 @@
'use client'
import type { FormFieldBlock, Form as FormType } from '@payloadcms/plugin-form-builder/types'
import { useRouter } from 'next/navigation'
import React, { useCallback, useState } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import RichText from '@/components/RichText'
import { Button } from '@/components/ui/button'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { fields } from './fields'
import { getClientSideURL } from '@/utilities/getURL'
export type FormBlockType = {
blockName?: string
blockType?: 'formBlock'
enableIntro: boolean
form: FormType
introContent?: SerializedEditorState
}
export const FormBlock: React.FC<
{
id?: string
} & FormBlockType
> = (props) => {
const {
enableIntro,
form: formFromProps,
form: { id: formID, confirmationMessage, confirmationType, redirect, submitButtonLabel } = {},
introContent,
} = props
const formMethods = useForm({
defaultValues: formFromProps.fields,
})
const {
control,
formState: { errors },
handleSubmit,
register,
} = formMethods
const [isLoading, setIsLoading] = useState(false)
const [hasSubmitted, setHasSubmitted] = useState<boolean>()
const [error, setError] = useState<{ message: string; status?: string } | undefined>()
const router = useRouter()
const onSubmit = useCallback(
(data: FormFieldBlock[]) => {
let loadingTimerID: ReturnType<typeof setTimeout>
const submitForm = async () => {
setError(undefined)
const dataToSend = Object.entries(data).map(([name, value]) => ({
field: name,
value,
}))
// delay loading indicator by 1s
loadingTimerID = setTimeout(() => {
setIsLoading(true)
}, 1000)
try {
const req = await fetch(`${getClientSideURL()}/api/form-submissions`, {
body: JSON.stringify({
form: formID,
submissionData: dataToSend,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const res = await req.json()
clearTimeout(loadingTimerID)
if (req.status >= 400) {
setIsLoading(false)
setError({
message: res.errors?.[0]?.message || 'Internal Server Error',
status: res.status,
})
return
}
setIsLoading(false)
setHasSubmitted(true)
if (confirmationType === 'redirect' && redirect) {
const { url } = redirect
const redirectUrl = url
if (redirectUrl) router.push(redirectUrl)
}
} catch (err) {
console.warn(err)
setIsLoading(false)
setError({
message: 'Something went wrong.',
})
}
}
void submitForm()
},
[router, formID, redirect, confirmationType]
)
return (
<div className="container lg:max-w-[48rem]">
{enableIntro && introContent && !hasSubmitted && (
<RichText className="mb-8 lg:mb-12" data={introContent} enableGutter={false} />
)}
<div className="p-4 lg:p-6 border border-border rounded-[0.8rem]">
<FormProvider {...formMethods}>
{!isLoading && hasSubmitted && confirmationType === 'message' && (
<RichText data={confirmationMessage} />
)}
{isLoading && !hasSubmitted && <p>Loading, please wait...</p>}
{error && <div>{`${error.status || '500'}: ${error.message || ''}`}</div>}
{!hasSubmitted && (
<form id={formID} onSubmit={handleSubmit(onSubmit)}>
<div className="mb-4 last:mb-0">
{formFromProps &&
formFromProps.fields &&
formFromProps.fields?.map((field, index) => {
const Field: React.FC<any> = fields?.[field.blockType as keyof typeof fields]
if (Field) {
return (
<div className="mb-6 last:mb-0" key={index}>
<Field
form={formFromProps}
{...field}
{...formMethods}
control={control}
errors={errors}
register={register}
/>
</div>
)
}
return null
})}
</div>
<Button form={formID} type="submit" variant="default">
{submitButtonLabel}
</Button>
</form>
)}
</FormProvider>
</div>
</div>
)
}
@@ -0,0 +1,65 @@
import type { CountryField } from '@payloadcms/plugin-form-builder/types'
import type { Control, FieldErrorsImpl } from 'react-hook-form'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import React from 'react'
import { Controller } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import { countryOptions } from './options'
export const Country: React.FC<
CountryField & {
control: Control
errors: Partial<FieldErrorsImpl>
}
> = ({ name, control, errors, label, required, width }) => {
return (
<Width width={width}>
<Label className="" htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Controller
control={control}
defaultValue=""
name={name}
render={({ field: { onChange, value } }) => {
const controlledValue = countryOptions.find((t) => t.value === value)
return (
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
<SelectTrigger className="w-full" id={name}>
<SelectValue placeholder={label} />
</SelectTrigger>
<SelectContent>
{countryOptions.map(({ label, value }) => {
return (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
)
})}
</SelectContent>
</Select>
)
}}
rules={{ required }}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+982
View File
@@ -0,0 +1,982 @@
export const countryOptions = [
{
label: 'Afghanistan',
value: 'AF',
},
{
label: 'Åland Islands',
value: 'AX',
},
{
label: 'Albania',
value: 'AL',
},
{
label: 'Algeria',
value: 'DZ',
},
{
label: 'American Samoa',
value: 'AS',
},
{
label: 'Andorra',
value: 'AD',
},
{
label: 'Angola',
value: 'AO',
},
{
label: 'Anguilla',
value: 'AI',
},
{
label: 'Antarctica',
value: 'AQ',
},
{
label: 'Antigua and Barbuda',
value: 'AG',
},
{
label: 'Argentina',
value: 'AR',
},
{
label: 'Armenia',
value: 'AM',
},
{
label: 'Aruba',
value: 'AW',
},
{
label: 'Australia',
value: 'AU',
},
{
label: 'Austria',
value: 'AT',
},
{
label: 'Azerbaijan',
value: 'AZ',
},
{
label: 'Bahamas',
value: 'BS',
},
{
label: 'Bahrain',
value: 'BH',
},
{
label: 'Bangladesh',
value: 'BD',
},
{
label: 'Barbados',
value: 'BB',
},
{
label: 'Belarus',
value: 'BY',
},
{
label: 'Belgium',
value: 'BE',
},
{
label: 'Belize',
value: 'BZ',
},
{
label: 'Benin',
value: 'BJ',
},
{
label: 'Bermuda',
value: 'BM',
},
{
label: 'Bhutan',
value: 'BT',
},
{
label: 'Bolivia',
value: 'BO',
},
{
label: 'Bosnia and Herzegovina',
value: 'BA',
},
{
label: 'Botswana',
value: 'BW',
},
{
label: 'Bouvet Island',
value: 'BV',
},
{
label: 'Brazil',
value: 'BR',
},
{
label: 'British Indian Ocean Territory',
value: 'IO',
},
{
label: 'Brunei Darussalam',
value: 'BN',
},
{
label: 'Bulgaria',
value: 'BG',
},
{
label: 'Burkina Faso',
value: 'BF',
},
{
label: 'Burundi',
value: 'BI',
},
{
label: 'Cambodia',
value: 'KH',
},
{
label: 'Cameroon',
value: 'CM',
},
{
label: 'Canada',
value: 'CA',
},
{
label: 'Cape Verde',
value: 'CV',
},
{
label: 'Cayman Islands',
value: 'KY',
},
{
label: 'Central African Republic',
value: 'CF',
},
{
label: 'Chad',
value: 'TD',
},
{
label: 'Chile',
value: 'CL',
},
{
label: 'China',
value: 'CN',
},
{
label: 'Christmas Island',
value: 'CX',
},
{
label: 'Cocos (Keeling) Islands',
value: 'CC',
},
{
label: 'Colombia',
value: 'CO',
},
{
label: 'Comoros',
value: 'KM',
},
{
label: 'Congo',
value: 'CG',
},
{
label: 'Congo, The Democratic Republic of the',
value: 'CD',
},
{
label: 'Cook Islands',
value: 'CK',
},
{
label: 'Costa Rica',
value: 'CR',
},
{
label: "Cote D'Ivoire",
value: 'CI',
},
{
label: 'Croatia',
value: 'HR',
},
{
label: 'Cuba',
value: 'CU',
},
{
label: 'Cyprus',
value: 'CY',
},
{
label: 'Czech Republic',
value: 'CZ',
},
{
label: 'Denmark',
value: 'DK',
},
{
label: 'Djibouti',
value: 'DJ',
},
{
label: 'Dominica',
value: 'DM',
},
{
label: 'Dominican Republic',
value: 'DO',
},
{
label: 'Ecuador',
value: 'EC',
},
{
label: 'Egypt',
value: 'EG',
},
{
label: 'El Salvador',
value: 'SV',
},
{
label: 'Equatorial Guinea',
value: 'GQ',
},
{
label: 'Eritrea',
value: 'ER',
},
{
label: 'Estonia',
value: 'EE',
},
{
label: 'Ethiopia',
value: 'ET',
},
{
label: 'Falkland Islands (Malvinas)',
value: 'FK',
},
{
label: 'Faroe Islands',
value: 'FO',
},
{
label: 'Fiji',
value: 'FJ',
},
{
label: 'Finland',
value: 'FI',
},
{
label: 'France',
value: 'FR',
},
{
label: 'French Guiana',
value: 'GF',
},
{
label: 'French Polynesia',
value: 'PF',
},
{
label: 'French Southern Territories',
value: 'TF',
},
{
label: 'Gabon',
value: 'GA',
},
{
label: 'Gambia',
value: 'GM',
},
{
label: 'Georgia',
value: 'GE',
},
{
label: 'Germany',
value: 'DE',
},
{
label: 'Ghana',
value: 'GH',
},
{
label: 'Gibraltar',
value: 'GI',
},
{
label: 'Greece',
value: 'GR',
},
{
label: 'Greenland',
value: 'GL',
},
{
label: 'Grenada',
value: 'GD',
},
{
label: 'Guadeloupe',
value: 'GP',
},
{
label: 'Guam',
value: 'GU',
},
{
label: 'Guatemala',
value: 'GT',
},
{
label: 'Guernsey',
value: 'GG',
},
{
label: 'Guinea',
value: 'GN',
},
{
label: 'Guinea-Bissau',
value: 'GW',
},
{
label: 'Guyana',
value: 'GY',
},
{
label: 'Haiti',
value: 'HT',
},
{
label: 'Heard Island and Mcdonald Islands',
value: 'HM',
},
{
label: 'Holy See (Vatican City State)',
value: 'VA',
},
{
label: 'Honduras',
value: 'HN',
},
{
label: 'Hong Kong',
value: 'HK',
},
{
label: 'Hungary',
value: 'HU',
},
{
label: 'Iceland',
value: 'IS',
},
{
label: 'India',
value: 'IN',
},
{
label: 'Indonesia',
value: 'ID',
},
{
label: 'Iran, Islamic Republic Of',
value: 'IR',
},
{
label: 'Iraq',
value: 'IQ',
},
{
label: 'Ireland',
value: 'IE',
},
{
label: 'Isle of Man',
value: 'IM',
},
{
label: 'Israel',
value: 'IL',
},
{
label: 'Italy',
value: 'IT',
},
{
label: 'Jamaica',
value: 'JM',
},
{
label: 'Japan',
value: 'JP',
},
{
label: 'Jersey',
value: 'JE',
},
{
label: 'Jordan',
value: 'JO',
},
{
label: 'Kazakhstan',
value: 'KZ',
},
{
label: 'Kenya',
value: 'KE',
},
{
label: 'Kiribati',
value: 'KI',
},
{
label: "Democratic People's Republic of Korea",
value: 'KP',
},
{
label: 'Korea, Republic of',
value: 'KR',
},
{
label: 'Kosovo',
value: 'XK',
},
{
label: 'Kuwait',
value: 'KW',
},
{
label: 'Kyrgyzstan',
value: 'KG',
},
{
label: "Lao People's Democratic Republic",
value: 'LA',
},
{
label: 'Latvia',
value: 'LV',
},
{
label: 'Lebanon',
value: 'LB',
},
{
label: 'Lesotho',
value: 'LS',
},
{
label: 'Liberia',
value: 'LR',
},
{
label: 'Libyan Arab Jamahiriya',
value: 'LY',
},
{
label: 'Liechtenstein',
value: 'LI',
},
{
label: 'Lithuania',
value: 'LT',
},
{
label: 'Luxembourg',
value: 'LU',
},
{
label: 'Macao',
value: 'MO',
},
{
label: 'Macedonia, The Former Yugoslav Republic of',
value: 'MK',
},
{
label: 'Madagascar',
value: 'MG',
},
{
label: 'Malawi',
value: 'MW',
},
{
label: 'Malaysia',
value: 'MY',
},
{
label: 'Maldives',
value: 'MV',
},
{
label: 'Mali',
value: 'ML',
},
{
label: 'Malta',
value: 'MT',
},
{
label: 'Marshall Islands',
value: 'MH',
},
{
label: 'Martinique',
value: 'MQ',
},
{
label: 'Mauritania',
value: 'MR',
},
{
label: 'Mauritius',
value: 'MU',
},
{
label: 'Mayotte',
value: 'YT',
},
{
label: 'Mexico',
value: 'MX',
},
{
label: 'Micronesia, Federated States of',
value: 'FM',
},
{
label: 'Moldova, Republic of',
value: 'MD',
},
{
label: 'Monaco',
value: 'MC',
},
{
label: 'Mongolia',
value: 'MN',
},
{
label: 'Montenegro',
value: 'ME',
},
{
label: 'Montserrat',
value: 'MS',
},
{
label: 'Morocco',
value: 'MA',
},
{
label: 'Mozambique',
value: 'MZ',
},
{
label: 'Myanmar',
value: 'MM',
},
{
label: 'Namibia',
value: 'NA',
},
{
label: 'Nauru',
value: 'NR',
},
{
label: 'Nepal',
value: 'NP',
},
{
label: 'Netherlands',
value: 'NL',
},
{
label: 'Netherlands Antilles',
value: 'AN',
},
{
label: 'New Caledonia',
value: 'NC',
},
{
label: 'New Zealand',
value: 'NZ',
},
{
label: 'Nicaragua',
value: 'NI',
},
{
label: 'Niger',
value: 'NE',
},
{
label: 'Nigeria',
value: 'NG',
},
{
label: 'Niue',
value: 'NU',
},
{
label: 'Norfolk Island',
value: 'NF',
},
{
label: 'Northern Mariana Islands',
value: 'MP',
},
{
label: 'Norway',
value: 'NO',
},
{
label: 'Oman',
value: 'OM',
},
{
label: 'Pakistan',
value: 'PK',
},
{
label: 'Palau',
value: 'PW',
},
{
label: 'Palestinian Territory, Occupied',
value: 'PS',
},
{
label: 'Panama',
value: 'PA',
},
{
label: 'Papua New Guinea',
value: 'PG',
},
{
label: 'Paraguay',
value: 'PY',
},
{
label: 'Peru',
value: 'PE',
},
{
label: 'Philippines',
value: 'PH',
},
{
label: 'Pitcairn',
value: 'PN',
},
{
label: 'Poland',
value: 'PL',
},
{
label: 'Portugal',
value: 'PT',
},
{
label: 'Puerto Rico',
value: 'PR',
},
{
label: 'Qatar',
value: 'QA',
},
{
label: 'Reunion',
value: 'RE',
},
{
label: 'Romania',
value: 'RO',
},
{
label: 'Russian Federation',
value: 'RU',
},
{
label: 'Rwanda',
value: 'RW',
},
{
label: 'Saint Helena',
value: 'SH',
},
{
label: 'Saint Kitts and Nevis',
value: 'KN',
},
{
label: 'Saint Lucia',
value: 'LC',
},
{
label: 'Saint Pierre and Miquelon',
value: 'PM',
},
{
label: 'Saint Vincent and the Grenadines',
value: 'VC',
},
{
label: 'Samoa',
value: 'WS',
},
{
label: 'San Marino',
value: 'SM',
},
{
label: 'Sao Tome and Principe',
value: 'ST',
},
{
label: 'Saudi Arabia',
value: 'SA',
},
{
label: 'Senegal',
value: 'SN',
},
{
label: 'Serbia',
value: 'RS',
},
{
label: 'Seychelles',
value: 'SC',
},
{
label: 'Sierra Leone',
value: 'SL',
},
{
label: 'Singapore',
value: 'SG',
},
{
label: 'Slovakia',
value: 'SK',
},
{
label: 'Slovenia',
value: 'SI',
},
{
label: 'Solomon Islands',
value: 'SB',
},
{
label: 'Somalia',
value: 'SO',
},
{
label: 'South Africa',
value: 'ZA',
},
{
label: 'South Georgia and the South Sandwich Islands',
value: 'GS',
},
{
label: 'Spain',
value: 'ES',
},
{
label: 'Sri Lanka',
value: 'LK',
},
{
label: 'Sudan',
value: 'SD',
},
{
label: 'Suriname',
value: 'SR',
},
{
label: 'Svalbard and Jan Mayen',
value: 'SJ',
},
{
label: 'Swaziland',
value: 'SZ',
},
{
label: 'Sweden',
value: 'SE',
},
{
label: 'Switzerland',
value: 'CH',
},
{
label: 'Syrian Arab Republic',
value: 'SY',
},
{
label: 'Taiwan',
value: 'TW',
},
{
label: 'Tajikistan',
value: 'TJ',
},
{
label: 'Tanzania, United Republic of',
value: 'TZ',
},
{
label: 'Thailand',
value: 'TH',
},
{
label: 'Timor-Leste',
value: 'TL',
},
{
label: 'Togo',
value: 'TG',
},
{
label: 'Tokelau',
value: 'TK',
},
{
label: 'Tonga',
value: 'TO',
},
{
label: 'Trinidad and Tobago',
value: 'TT',
},
{
label: 'Tunisia',
value: 'TN',
},
{
label: 'Turkey',
value: 'TR',
},
{
label: 'Turkmenistan',
value: 'TM',
},
{
label: 'Turks and Caicos Islands',
value: 'TC',
},
{
label: 'Tuvalu',
value: 'TV',
},
{
label: 'Uganda',
value: 'UG',
},
{
label: 'Ukraine',
value: 'UA',
},
{
label: 'United Arab Emirates',
value: 'AE',
},
{
label: 'United Kingdom',
value: 'GB',
},
{
label: 'United States',
value: 'US',
},
{
label: 'United States Minor Outlying Islands',
value: 'UM',
},
{
label: 'Uruguay',
value: 'UY',
},
{
label: 'Uzbekistan',
value: 'UZ',
},
{
label: 'Vanuatu',
value: 'VU',
},
{
label: 'Venezuela',
value: 'VE',
},
{
label: 'Viet Nam',
value: 'VN',
},
{
label: 'Virgin Islands, British',
value: 'VG',
},
{
label: 'Virgin Islands, U.S.',
value: 'VI',
},
{
label: 'Wallis and Futuna',
value: 'WF',
},
{
label: 'Western Sahara',
value: 'EH',
},
{
label: 'Yemen',
value: 'YE',
},
{
label: 'Zambia',
value: 'ZM',
},
{
label: 'Zimbabwe',
value: 'ZW',
},
]
+38
View File
@@ -0,0 +1,38 @@
import type { EmailField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
export const Email: React.FC<
EmailField & {
errors: Partial<FieldErrorsImpl>
register: UseFormRegister<FieldValues>
}
> = ({ name, defaultValue, errors, label, register, required, width }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Input
defaultValue={defaultValue}
id={name}
type="text"
{...register(name, { pattern: /^\S[^\s@]*@\S+$/, required })}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
import * as React from 'react'
import { useFormContext } from 'react-hook-form'
export const Error = ({ name }: { name: string }) => {
const {
formState: { errors },
} = useFormContext()
return (
<div className="mt-2 text-red-500 text-sm">
{(errors[name]?.message as string) || 'This field is required'}
</div>
)
}
@@ -0,0 +1,13 @@
import RichText from '@/components/RichText'
import React from 'react'
import { Width } from '../Width'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => {
return (
<Width className="my-12" width="100">
{message && <RichText data={message} />}
</Width>
)
}
+36
View File
@@ -0,0 +1,36 @@
import type { TextField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
export const Number: React.FC<
TextField & {
errors: Partial<FieldErrorsImpl>
register: UseFormRegister<FieldValues>
}
> = ({ name, defaultValue, errors, label, register, required, width }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Input
defaultValue={defaultValue}
id={name}
type="number"
{...register(name, { required })}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+63
View File
@@ -0,0 +1,63 @@
import type { SelectField } from '@payloadcms/plugin-form-builder/types'
import type { Control, FieldErrorsImpl } from 'react-hook-form'
import { Label } from '@/components/ui/label'
import {
Select as SelectComponent,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import React from 'react'
import { Controller } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
export const Select: React.FC<
SelectField & {
control: Control
errors: Partial<FieldErrorsImpl>
}
> = ({ name, control, errors, label, options, required, width, defaultValue }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Controller
control={control}
defaultValue={defaultValue}
name={name}
render={({ field: { onChange, value } }) => {
const controlledValue = options.find((t) => t.value === value)
return (
<SelectComponent onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
<SelectTrigger className="w-full" id={name}>
<SelectValue placeholder={label} />
</SelectTrigger>
<SelectContent>
{options.map(({ label, value }) => {
return (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
)
})}
</SelectContent>
</SelectComponent>
)
}}
rules={{ required }}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+64
View File
@@ -0,0 +1,64 @@
import type { StateField } from '@payloadcms/plugin-form-builder/types'
import type { Control, FieldErrorsImpl } from 'react-hook-form'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import React from 'react'
import { Controller } from 'react-hook-form'
import { Error } from '../Error'
import { Width } from '../Width'
import { stateOptions } from './options'
export const State: React.FC<
StateField & {
control: Control
errors: Partial<FieldErrorsImpl>
}
> = ({ name, control, errors, label, required, width }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Controller
control={control}
defaultValue=""
name={name}
render={({ field: { onChange, value } }) => {
const controlledValue = stateOptions.find((t) => t.value === value)
return (
<Select onValueChange={(val) => onChange(val)} value={controlledValue?.value}>
<SelectTrigger className="w-full" id={name}>
<SelectValue placeholder={label} />
</SelectTrigger>
<SelectContent>
{stateOptions.map(({ label, value }) => {
return (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
)
})}
</SelectContent>
</Select>
)
}}
rules={{ required }}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+52
View File
@@ -0,0 +1,52 @@
export const stateOptions = [
{ label: 'Alabama', value: 'AL' },
{ label: 'Alaska', value: 'AK' },
{ label: 'Arizona', value: 'AZ' },
{ label: 'Arkansas', value: 'AR' },
{ label: 'California', value: 'CA' },
{ label: 'Colorado', value: 'CO' },
{ label: 'Connecticut', value: 'CT' },
{ label: 'Delaware', value: 'DE' },
{ label: 'Florida', value: 'FL' },
{ label: 'Georgia', value: 'GA' },
{ label: 'Hawaii', value: 'HI' },
{ label: 'Idaho', value: 'ID' },
{ label: 'Illinois', value: 'IL' },
{ label: 'Indiana', value: 'IN' },
{ label: 'Iowa', value: 'IA' },
{ label: 'Kansas', value: 'KS' },
{ label: 'Kentucky', value: 'KY' },
{ label: 'Louisiana', value: 'LA' },
{ label: 'Maine', value: 'ME' },
{ label: 'Maryland', value: 'MD' },
{ label: 'Massachusetts', value: 'MA' },
{ label: 'Michigan', value: 'MI' },
{ label: 'Minnesota', value: 'MN' },
{ label: 'Mississippi', value: 'MS' },
{ label: 'Missouri', value: 'MO' },
{ label: 'Montana', value: 'MT' },
{ label: 'Nebraska', value: 'NE' },
{ label: 'Nevada', value: 'NV' },
{ label: 'New Hampshire', value: 'NH' },
{ label: 'New Jersey', value: 'NJ' },
{ label: 'New Mexico', value: 'NM' },
{ label: 'New York', value: 'NY' },
{ label: 'North Carolina', value: 'NC' },
{ label: 'North Dakota', value: 'ND' },
{ label: 'Ohio', value: 'OH' },
{ label: 'Oklahoma', value: 'OK' },
{ label: 'Oregon', value: 'OR' },
{ label: 'Pennsylvania', value: 'PA' },
{ label: 'Rhode Island', value: 'RI' },
{ label: 'South Carolina', value: 'SC' },
{ label: 'South Dakota', value: 'SD' },
{ label: 'Tennessee', value: 'TN' },
{ label: 'Texas', value: 'TX' },
{ label: 'Utah', value: 'UT' },
{ label: 'Vermont', value: 'VT' },
{ label: 'Virginia', value: 'VA' },
{ label: 'Washington', value: 'WA' },
{ label: 'West Virginia', value: 'WV' },
{ label: 'Wisconsin', value: 'WI' },
{ label: 'Wyoming', value: 'WY' },
]
+32
View File
@@ -0,0 +1,32 @@
import type { TextField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
export const Text: React.FC<
TextField & {
errors: Partial<FieldErrorsImpl>
register: UseFormRegister<FieldValues>
}
> = ({ name, defaultValue, errors, label, register, required, width }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<Input defaultValue={defaultValue} id={name} type="text" {...register(name, { required })} />
{errors[name] && <Error name={name} />}
</Width>
)
}
@@ -0,0 +1,40 @@
import type { TextField } from '@payloadcms/plugin-form-builder/types'
import type { FieldErrorsImpl, FieldValues, UseFormRegister } from 'react-hook-form'
import { Label } from '@/components/ui/label'
import { Textarea as TextAreaComponent } from '@/components/ui/textarea'
import React from 'react'
import { Error } from '../Error'
import { Width } from '../Width'
export const Textarea: React.FC<
TextField & {
errors: Partial<FieldErrorsImpl>
register: UseFormRegister<FieldValues>
rows?: number
}
> = ({ name, defaultValue, errors, label, register, required, rows = 3, width }) => {
return (
<Width width={width}>
<Label htmlFor={name}>
{label}
{required && (
<span className="required">
* <span className="sr-only">(required)</span>
</span>
)}
</Label>
<TextAreaComponent
defaultValue={defaultValue}
id={name}
rows={rows}
{...register(name, { required: required })}
/>
{errors[name] && <Error name={name} />}
</Width>
)
}
+13
View File
@@ -0,0 +1,13 @@
import * as React from 'react'
export const Width: React.FC<{
children: React.ReactNode
className?: string
width?: number | string
}> = ({ children, className, width }) => {
return (
<div className={className} style={{ maxWidth: width ? `${width}%` : undefined }}>
{children}
</div>
)
}
+51
View File
@@ -0,0 +1,51 @@
import type { Block } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
export const FormBlock: Block = {
slug: 'formBlock',
interfaceName: 'FormBlock',
fields: [
{
name: 'form',
type: 'relationship',
relationTo: 'forms',
required: true,
},
{
name: 'enableIntro',
type: 'checkbox',
label: 'Enable Intro Content',
},
{
name: 'introContent',
type: 'richText',
admin: {
condition: (_, { enableIntro }) => Boolean(enableIntro),
},
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
]
},
}),
label: 'Intro Content',
},
],
graphQL: {
singularName: 'FormBlock',
},
labels: {
plural: 'Form Blocks',
singular: 'Form Block',
},
}
+21
View File
@@ -0,0 +1,21 @@
import { Checkbox } from './Checkbox'
import { Country } from './Country'
import { Email } from './Email'
import { Message } from './Message'
import { Number } from './Number'
import { Select } from './Select'
import { State } from './State'
import { Text } from './Text'
import { Textarea } from './Textarea'
export const fields = {
checkbox: Checkbox,
country: Country,
email: Email,
message: Message,
number: Number,
select: Select,
state: State,
text: Text,
textarea: Textarea,
}
@@ -0,0 +1,67 @@
import type { StaticImageData } from 'next/image'
import { cn } from '@/utilities/ui'
import React from 'react'
import RichText from '@/components/RichText'
import type { MediaBlock as MediaBlockProps } from '@/payload-types'
import { Media } from '../../components/Media'
type Props = MediaBlockProps & {
breakout?: boolean
captionClassName?: string
className?: string
enableGutter?: boolean
imgClassName?: string
staticImage?: StaticImageData
disableInnerContainer?: boolean
}
export const MediaBlock: React.FC<Props> = (props) => {
const {
captionClassName,
className,
enableGutter = true,
imgClassName,
media,
staticImage,
disableInnerContainer,
} = props
let caption
if (media && typeof media === 'object') caption = media.caption
return (
<div
className={cn(
'',
{
container: enableGutter,
},
className
)}
>
{(media || staticImage) && (
<Media
imgClassName={cn('border border-border rounded-[0.8rem]', imgClassName)}
resource={media}
src={staticImage}
/>
)}
{caption && (
<div
className={cn(
'mt-6',
{
container: !disableInnerContainer,
},
captionClassName
)}
>
<RichText data={caption} enableGutter={false} />
</div>
)}
</div>
)
}
+14
View File
@@ -0,0 +1,14 @@
import type { Block } from 'payload'
export const MediaBlock: Block = {
slug: 'mediaBlock',
interfaceName: 'MediaBlock',
fields: [
{
name: 'media',
type: 'upload',
relationTo: 'media',
required: true,
},
],
}
Loaded 100 of 6111 files, more files were not shown because too many files have changed in this diff. Show more