├── .cursor └── rules │ └── 21st.mdc ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── apps ├── backend │ ├── bun.lockb │ ├── nixpacks.toml │ ├── package.json │ ├── serve.ts │ └── src │ │ ├── bundler │ │ ├── index.ts │ │ ├── project.ts │ │ ├── types.ts │ │ └── vite.ts │ │ ├── css-processor.ts │ │ ├── r2.ts │ │ ├── routes │ │ └── index.ts │ │ ├── server │ │ ├── editor.ts │ │ └── index.ts │ │ └── video-converter.ts └── web │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── app │ ├── (utility) │ │ ├── privacy │ │ │ └── page.tsx │ │ └── terms │ │ │ └── page.tsx │ ├── SessionRecorder.tsx │ ├── [username] │ │ ├── [component_slug] │ │ │ ├── [demo_slug] │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── opengraph-image.tsx │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ ├── opengraph-image.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── admin │ │ ├── leaderboard │ │ │ └── page.tsx │ │ └── submissions │ │ │ └── page.tsx │ ├── api-access │ │ └── page.tsx │ ├── api │ │ ├── author │ │ │ └── stats │ │ │ │ └── route.ts │ │ ├── bundle │ │ │ └── route.ts │ │ ├── components │ │ │ ├── import │ │ │ │ └── route.ts │ │ │ └── purchase │ │ │ │ └── route.ts │ │ ├── cron │ │ │ └── gen-usage-embeddings │ │ │ │ └── route.ts │ │ ├── emails │ │ │ └── submission-status │ │ │ │ └── route.ts │ │ ├── magic-search │ │ │ └── route.ts │ │ ├── magic │ │ │ ├── check │ │ │ │ └── route.ts │ │ │ └── use │ │ │ │ └── route.ts │ │ ├── prompts │ │ │ └── route.ts │ │ ├── public-dashboard │ │ │ └── route.ts │ │ ├── purge-cache │ │ │ └── route.ts │ │ ├── r │ │ │ └── [username] │ │ │ │ └── [component_slug] │ │ │ │ ├── route.ts │ │ │ │ └── types.ts │ │ ├── sandbox │ │ │ ├── connect │ │ │ │ └── route.ts │ │ │ ├── edit │ │ │ │ └── route.ts │ │ │ ├── new │ │ │ │ └── route.ts │ │ │ └── publish │ │ │ │ └── route.ts │ │ ├── search-mcp │ │ │ └── route.ts │ │ ├── search │ │ │ └── route.ts │ │ ├── stripe │ │ │ ├── account │ │ │ │ └── route.ts │ │ │ ├── cancel-subscription │ │ │ │ └── route.ts │ │ │ ├── create-checkout-bundle │ │ │ │ └── route.ts │ │ │ ├── create-checkout │ │ │ │ └── route.ts │ │ │ ├── get-account-link │ │ │ │ └── route.ts │ │ │ ├── get-dashboard-link │ │ │ │ └── route.ts │ │ │ ├── get-invoices │ │ │ │ └── route.ts │ │ │ ├── get-subscription │ │ │ │ └── route.ts │ │ │ └── webhook │ │ │ │ ├── v1 │ │ │ │ └── route.ts │ │ │ │ └── v2 │ │ │ │ └── route.ts │ │ ├── studio │ │ │ ├── merge-styles │ │ │ │ ├── globals │ │ │ │ │ └── route.ts │ │ │ │ └── tailwind │ │ │ │ │ └── route.ts │ │ │ └── preprocess-component │ │ │ │ └── route.ts │ │ ├── subscribe │ │ │ └── route.ts │ │ ├── subscription │ │ │ ├── current-plan │ │ │ │ └── route.ts │ │ │ └── stripe-cron │ │ │ │ └── route.ts │ │ ├── svgl │ │ │ └── route.ts │ │ ├── user │ │ │ ├── delete-account │ │ │ │ └── route.ts │ │ │ └── profile │ │ │ │ ├── check-username │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ └── webhooks │ │ │ └── clerk │ │ │ └── route.ts │ ├── c │ │ └── [collection_slug] │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ ├── contest │ │ ├── archive │ │ │ └── [week] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ ├── leaderboard │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── globals.css │ ├── import-old │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── magic-chat │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── magic │ │ ├── console │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── onboarding │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── maintenance │ │ └── page.tsx │ ├── manifest.ts │ ├── our-story │ │ ├── founder-photos.tsx │ │ └── page.tsx │ ├── page.client.tsx │ ├── page.tsx │ ├── pricing │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── providers.tsx │ ├── public-dashboard │ │ ├── page.client.tsx │ │ └── page.tsx │ ├── publish │ │ ├── demo │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── template │ │ │ └── page.tsx │ ├── q │ │ └── [query] │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ ├── s │ │ └── [tag_slug] │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ ├── settings │ │ ├── billing │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── profile │ │ │ ├── page.client.tsx │ │ │ └── page.tsx │ │ └── rules │ │ │ ├── [id] │ │ │ └── page.tsx │ │ │ ├── new │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── sitemap.ts │ ├── studio │ │ ├── [username] │ │ │ ├── analytics │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── bundles │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── monetization │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ └── sandbox │ │ │ │ └── [sandboxId] │ │ │ │ ├── page.client.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── publish │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ └── page.tsx │ └── templates │ │ └── page.tsx │ ├── components.json │ ├── components │ ├── component-card.tsx │ ├── features │ │ ├── admin │ │ │ ├── AddToContestModal.tsx │ │ │ ├── AdminHeader.tsx │ │ │ ├── DeleteComponentDialog.tsx │ │ │ ├── EditDemoModal.tsx │ │ │ ├── ManageSubmissionModal.tsx │ │ │ ├── NonAdminPlaceholder.tsx │ │ │ ├── PaginationControls.tsx │ │ │ ├── SubmissionCard.tsx │ │ │ ├── SubmissionStatusFilter.tsx │ │ │ ├── db-links.tsx │ │ │ ├── hooks │ │ │ │ └── useSubmissions.ts │ │ │ ├── types.ts │ │ │ └── user-picker.tsx │ │ ├── api │ │ │ ├── api-docs.tsx │ │ │ ├── api-key-manager.tsx │ │ │ └── terms-dialog.tsx │ │ ├── auth │ │ │ └── login-form.tsx │ │ ├── bolt │ │ │ ├── bolt-banner.tsx │ │ │ ├── bolt-bg-1.png │ │ │ └── bolt-text.png │ │ ├── bundles │ │ │ ├── bundle-item.tsx │ │ │ ├── bundles-layout.tsx │ │ │ └── plans-dialog.tsx │ │ ├── categories │ │ │ ├── category-card.tsx │ │ │ ├── category-list.tsx │ │ │ ├── category-preview-image.tsx │ │ │ └── category-video-preview.tsx │ │ ├── collections │ │ │ ├── collection-card.tsx │ │ │ ├── collection-header.tsx │ │ │ └── collections-list.tsx │ │ ├── component-page │ │ │ ├── component-preview.module.css │ │ │ ├── component-preview.tsx │ │ │ ├── feature-cards.tsx │ │ │ ├── info-section.tsx │ │ │ ├── legacy-flow-preview-renderer.tsx │ │ │ ├── new-flow-preview-render.tsx │ │ │ ├── pay-wall.tsx │ │ │ ├── preview-dialog.tsx │ │ │ └── preview-renderer.tsx │ │ ├── contest │ │ │ ├── leaderboard-card.tsx │ │ │ └── leaderboard-list.tsx │ │ ├── design-engineers │ │ │ ├── design-engineer-card.tsx │ │ │ └── design-engineers-list.tsx │ │ ├── home │ │ │ ├── home-layout.tsx │ │ │ └── horizontal-slider.tsx │ │ ├── import-old │ │ │ └── components │ │ │ │ ├── import-form.tsx │ │ │ │ ├── import-header.tsx │ │ │ │ └── url-input.tsx │ │ ├── list-card │ │ │ ├── card-image.tsx │ │ │ ├── card-video.tsx │ │ │ └── card.tsx │ │ ├── magic │ │ │ ├── atoms.ts │ │ │ ├── component-animation.tsx │ │ │ ├── faq.tsx │ │ │ ├── features.tsx │ │ │ ├── feedback-dialog.tsx │ │ │ ├── get-started │ │ │ │ ├── api-key-section.tsx │ │ │ │ ├── get-started-tabs.tsx │ │ │ │ ├── ide-instructions.tsx │ │ │ │ ├── onboarding-layout.tsx │ │ │ │ ├── onboarding-server-wrapper.tsx │ │ │ │ └── welcome-onboarding.tsx │ │ │ ├── hero.tsx │ │ │ ├── how-it-works.tsx │ │ │ ├── magic-banner.tsx │ │ │ ├── magic-header.tsx │ │ │ ├── onboarding │ │ │ │ └── steps │ │ │ │ │ ├── create-component-step.tsx │ │ │ │ │ ├── install-ide-step.tsx │ │ │ │ │ ├── select-ide-step.tsx │ │ │ │ │ ├── troubleshooting-step.tsx │ │ │ │ │ ├── upgrade-pro-step.tsx │ │ │ │ │ └── welcome-step.tsx │ │ │ ├── pricing.tsx │ │ │ ├── supported-editors.tsx │ │ │ └── troubleshooting.tsx │ │ ├── main-page │ │ │ ├── filter-chips.tsx │ │ │ ├── help.tsx │ │ │ ├── main-layout.tsx │ │ │ ├── main-page-header.tsx │ │ │ └── sidebar-layout.tsx │ │ ├── pricing │ │ │ ├── faq.tsx │ │ │ ├── plan-comparison-table.tsx │ │ │ ├── pricing-section.tsx │ │ │ └── pricing-tab.tsx │ │ ├── pro │ │ │ └── pro-list.tsx │ │ ├── profile │ │ │ └── edit-profile-dialog.tsx │ │ ├── prompt-rules │ │ │ ├── prompt-rule-display.tsx │ │ │ ├── prompt-rule-form.tsx │ │ │ ├── prompt-rule-selector.tsx │ │ │ └── prompt-rules-list.tsx │ │ ├── publish │ │ │ ├── components │ │ │ │ ├── alerts.tsx │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── delete-demo-dialog.tsx │ │ │ │ ├── edit-code-file-card.tsx │ │ │ │ ├── first-stap-layout.tsx │ │ │ │ ├── forms │ │ │ │ │ ├── component-details-form.tsx │ │ │ │ │ ├── component-form.tsx │ │ │ │ │ └── demo-form.tsx │ │ │ │ ├── preview-with-tabs.tsx │ │ │ │ ├── preview.tsx │ │ │ │ ├── publish-header.tsx │ │ │ │ └── success-dialog.tsx │ │ │ ├── config │ │ │ │ ├── editor-themes.ts │ │ │ │ └── utils.ts │ │ │ ├── hooks │ │ │ │ ├── use-code-inputs-auto-focus.ts │ │ │ │ ├── use-hooks.tsx │ │ │ │ ├── use-is-admin.ts │ │ │ │ ├── use-is-check-slug-available.ts │ │ │ │ ├── use-name-slug-form.ts │ │ │ │ ├── use-publish-as.ts │ │ │ │ ├── use-r2-upload.ts │ │ │ │ ├── use-template-draft.ts │ │ │ │ ├── use-template-media-upload.ts │ │ │ │ ├── use-template-video-dropzone.ts │ │ │ │ └── use-video-dropzone.ts │ │ │ ├── publish-layout.tsx │ │ │ ├── template │ │ │ │ ├── publish-template-form.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── template-details-form.tsx │ │ │ └── version-selector-dialog.tsx │ │ ├── settings │ │ │ ├── billing │ │ │ │ ├── billing-header.tsx │ │ │ │ ├── confirmation-dialog.tsx │ │ │ │ ├── invoices-list.tsx │ │ │ │ ├── pricing-table.tsx │ │ │ │ └── upgrade-confirmation-dialog.tsx │ │ │ ├── settings-mobile-nav.tsx │ │ │ └── settings-sidebar.tsx │ │ ├── stripe │ │ │ └── utils.ts │ │ ├── studio │ │ │ ├── analytics │ │ │ │ └── creator-stats-chart.tsx │ │ │ ├── editor │ │ │ │ ├── component-publish-dialog.tsx │ │ │ │ ├── context │ │ │ │ │ ├── editor-atoms.ts │ │ │ │ │ ├── editor-state.tsx │ │ │ │ │ └── editor-types.ts │ │ │ │ ├── editor-code-panel.tsx │ │ │ │ ├── editor.tsx │ │ │ │ ├── file-explorer.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── use-action-required.ts │ │ │ │ │ ├── use-component-processing.ts │ │ │ │ │ ├── use-css-compiler.ts │ │ │ │ │ ├── use-dependencies.ts │ │ │ │ │ ├── use-dialog-state.ts │ │ │ │ │ ├── use-editor-dialog.ts │ │ │ │ │ ├── use-editor-file.ts │ │ │ │ │ ├── use-file-management.ts │ │ │ │ │ ├── use-preview-ready.ts │ │ │ │ │ └── use-sandpack-config.ts │ │ │ │ ├── requirements-panel.tsx │ │ │ │ ├── sandpack-initial-content.tsx │ │ │ │ └── utils │ │ │ │ │ ├── component-lookup.ts │ │ │ │ │ └── sandpack-files.ts │ │ │ ├── monetization │ │ │ │ ├── partner-program-modal.tsx │ │ │ │ └── payout-history-table.tsx │ │ │ ├── publish │ │ │ │ ├── components │ │ │ │ │ └── forms │ │ │ │ │ │ ├── component-form.tsx │ │ │ │ │ │ └── demo-form.tsx │ │ │ │ ├── config │ │ │ │ │ └── utils.ts │ │ │ │ └── hooks │ │ │ │ │ ├── use-component-data.ts │ │ │ │ │ ├── use-hooks.ts │ │ │ │ │ ├── use-is-check-slug-available.ts │ │ │ │ │ ├── use-name-slug-form.ts │ │ │ │ │ ├── use-submit-component.ts │ │ │ │ │ └── use-video-dropzone.ts │ │ │ ├── sandbox │ │ │ │ ├── api.ts │ │ │ │ ├── components │ │ │ │ │ ├── add-registry-modal.tsx │ │ │ │ │ ├── editor-pane.tsx │ │ │ │ │ ├── file-explorer.tsx │ │ │ │ │ ├── file-tree.tsx │ │ │ │ │ ├── preview-pane.tsx │ │ │ │ │ ├── publish-header.tsx │ │ │ │ │ └── sandbox-header.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── use-file-system.ts │ │ │ │ │ └── use-sandbox.ts │ │ │ │ ├── utils.ts │ │ │ │ └── utils │ │ │ │ │ └── dependencies.ts │ │ │ ├── studio-layout.tsx │ │ │ └── ui │ │ │ │ ├── components-table.tsx │ │ │ │ ├── studio-header.tsx │ │ │ │ ├── studio-navigation.tsx │ │ │ │ ├── studio-sidebar.tsx │ │ │ │ └── visibility-toggle.tsx │ │ ├── tag-page │ │ │ └── tag-page-header.tsx │ │ ├── templates │ │ │ ├── template-card.tsx │ │ │ ├── template-preview-modal.tsx │ │ │ ├── template-video-preview.tsx │ │ │ ├── templates-list-seo.tsx │ │ │ └── templates-list.tsx │ │ └── user-page │ │ │ ├── user-bunldes-list.tsx │ │ │ ├── user-items-list.tsx │ │ │ └── user-page-header.tsx │ ├── hooks │ │ └── use-user-profile.ts │ ├── icons │ │ ├── WindsurfTealLogo.tsx │ │ ├── clap.tsx │ │ ├── cursor-dark.tsx │ │ ├── cursor-light.tsx │ │ ├── index.tsx │ │ ├── logout-icon.tsx │ │ ├── lovable.tsx │ │ ├── magic-patterns.tsx │ │ ├── shadcn-file.tsx │ │ ├── sitebrew.tsx │ │ ├── spinner.tsx │ │ ├── svgl.tsx │ │ ├── terminal.tsx │ │ ├── upload.tsx │ │ ├── upvote-icon.tsx │ │ ├── vscode.tsx │ │ └── workslow.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aurora-background.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── banner.tsx │ │ ├── bookmark-button.tsx │ │ ├── brand-assets-menu.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── checkout-dialog.tsx │ │ ├── circle-progress.tsx │ │ ├── code-editor-dialog.tsx │ │ ├── code.tsx │ │ ├── collapsible.tsx │ │ ├── command-menu.tsx │ │ ├── command.tsx │ │ ├── container.tsx │ │ ├── context-menu.tsx │ │ ├── copy-code-card-button.tsx │ │ ├── copy-prompt-dialog.tsx │ │ ├── delete-account-dialog.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── edit-component-dialog.tsx │ │ ├── error-page.tsx │ │ ├── footer.tsx │ │ ├── form.tsx │ │ ├── full-screen-button.tsx │ │ ├── github-stars-number.tsx │ │ ├── header.client.tsx │ │ ├── hero-pill.tsx │ │ ├── hero-section.tsx │ │ ├── hero-video-dialog.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── items-list.tsx │ │ ├── label.tsx │ │ ├── link-preview.tsx │ │ ├── loading-dialog.tsx │ │ ├── loading-spinner.tsx │ │ ├── logo.tsx │ │ ├── maintenance-page.tsx │ │ ├── mockup.tsx │ │ ├── multiselect.tsx │ │ ├── navigation-menu.tsx │ │ ├── newsletter-dialog.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── pricing-card.tsx │ │ ├── pricing-section.tsx │ │ ├── pricing-tab.tsx │ │ ├── pricing-table.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── shimmer-button.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── skeletons.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── tag.tsx │ │ ├── text-morph.tsx │ │ ├── text-shimmer.tsx │ │ ├── textarea.tsx │ │ ├── theme-toggle.tsx │ │ ├── toast.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── user-avatar.tsx │ ├── config │ └── invite-list.json │ ├── global.d.ts │ ├── hooks │ ├── use-analytics.ts │ ├── use-bundle-demo.ts │ ├── use-compile-css.ts │ ├── use-component-access.ts │ ├── use-debounce.tsx │ ├── use-debounced-state.ts │ ├── use-debug-mode.ts │ ├── use-image-upload.ts │ ├── use-intersection-observer.ts │ ├── use-media-query.ts │ ├── use-mobile.tsx │ ├── use-navigation.ts │ ├── use-prompt-rules.ts │ ├── use-sidebar-hotkey.ts │ ├── use-sidebar-visibility.ts │ ├── use-subscription.ts │ └── use-toast.ts │ ├── lib │ ├── amplitude.ts │ ├── api │ │ ├── bundle_purchases.ts │ │ ├── bundles.ts │ │ ├── components.ts │ │ ├── demos.ts │ │ ├── server │ │ │ ├── bundle_purchases.ts │ │ │ ├── bundles.ts │ │ │ ├── components.ts │ │ │ ├── demos.ts │ │ │ └── users.ts │ │ └── users.ts │ ├── atoms.ts │ ├── attribution-tracking.ts │ ├── clerk.ts │ ├── codesandbox-sdk.ts │ ├── config │ │ ├── magic-mcp.ts │ │ └── subscription-plans.ts │ ├── constants.ts │ ├── cookies.ts │ ├── defaults.ts │ ├── emails │ │ ├── invite-template.tsx │ │ ├── send-invites.ts │ │ ├── send-submission-status.ts │ │ └── submission-status-template.tsx │ ├── filters.client.ts │ ├── licenses.ts │ ├── navigation-with-magic.tsx │ ├── navigation.ts │ ├── parsers.ts │ ├── posthog.ts │ ├── prisma.ts │ ├── prompts.tsx │ ├── queries.server.ts │ ├── queries.ts │ ├── r2.ts │ ├── registry.test.ts │ ├── registry.ts │ ├── resend.ts │ ├── sandpack.tsx │ ├── server │ │ └── clerk.ts │ ├── shadcn-components.ts │ ├── store │ │ └── user-store.ts │ ├── stripe.ts │ ├── supabase.ts │ ├── supabase │ │ └── functions │ │ │ └── get_prompt.sql │ ├── user.ts │ ├── utils.ts │ └── utils │ │ ├── fetchFileTextContent.ts │ │ ├── transformData.ts │ │ ├── url.ts │ │ └── validateRouteParams.ts │ ├── middleware.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── prisma │ ├── schema-formatted.prisma │ └── schema.prisma │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── brand │ │ ├── 21st-brand.zip │ │ ├── 21st-logo-dark.png │ │ ├── 21st-logo-dark.svg │ │ ├── 21st-logo-white.png │ │ └── 21st-logo-white.svg │ ├── cline-first-step.png │ ├── css-file-dark.svg │ ├── css-file.svg │ ├── cursor-dark.svg │ ├── cursor-light.svg │ ├── dark-theme.svg │ ├── demo-file-dark.svg │ ├── demo-file.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── features-1.svg │ ├── features-2.svg │ ├── features-3.svg │ ├── github.svg │ ├── globe.svg │ ├── hero.webp │ ├── how-it-works-1.png │ ├── how-it-works-2.png │ ├── how-it-works-3.png │ ├── icon.png │ ├── light-theme.svg │ ├── loading.json │ ├── magic-agent-og-image.png │ ├── magic-chat-og.png │ ├── magic-chat-waitilist.png │ ├── magic-chat-waitilist.webp │ ├── magic-preview.png │ ├── magic-preview.webp │ ├── og-image-old.png │ ├── og-image.png │ ├── placeholder.svg │ ├── product-of-the-day.png │ ├── robots.txt │ ├── story │ │ ├── photo_2025-05-14 20.49.00.jpeg │ │ ├── photo_2025-05-14 20.49.02.jpeg │ │ ├── photo_2025-05-14 20.49.03.jpeg │ │ ├── photo_2025-05-14 20.49.05.jpeg │ │ ├── photo_2025-05-14 20.49.06.jpeg │ │ ├── photo_2025-05-14 20.49.08.jpeg │ │ └── photo_2025-05-14 20.49.10.jpeg │ ├── studio-background.webp │ ├── tsx-file-dark.svg │ ├── tsx-file.svg │ ├── tutorial-thumbnail.png │ ├── upload-icon-dark.png │ ├── upload-icon.png │ ├── window.svg │ └── x.svg │ ├── scripts │ ├── generate-embeddings.ts │ └── send-invites.ts │ ├── sql │ ├── prompt_rules.sql │ └── subscription_attribution_analysis.sql │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types │ ├── global.ts │ ├── json.d.ts │ ├── prompt-rules.ts │ └── supabase.ts │ ├── vercel.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── next.config.js ├── package.json ├── packages ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── backup-r2.js ├── embed-all-demos.js ├── package-lock.json └── package.json ├── search_results.json ├── supabase-demo-update-function.sql ├── supabase-email-notifications.sql ├── supabase ├── .gitignore ├── config.toml ├── functions │ ├── .vscode │ │ ├── extensions.json │ │ └── settings.json │ ├── ai-search-oai │ │ ├── .npmrc │ │ ├── deno.json │ │ └── index.ts │ ├── ai-search │ │ ├── deno.json │ │ └── index.ts │ ├── embed-oai │ │ ├── .npmrc │ │ ├── deno.json │ │ ├── deno.lock │ │ └── index.ts │ ├── embed │ │ └── index.ts │ ├── generate-embeddings │ │ ├── .npmrc │ │ ├── ai-config.ts │ │ ├── deno.json │ │ ├── deno.lock │ │ └── index.ts │ ├── hello │ │ └── index.ts │ ├── notify │ │ ├── deno.json │ │ └── index.ts │ ├── search-embeddings │ │ ├── .npmrc │ │ ├── deno.json │ │ ├── deno.lock │ │ └── index.ts │ └── search_demos_ai_oai_extended │ │ ├── .npmrc │ │ ├── deno.json │ │ └── index.ts └── seed.sql ├── turbo.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | .pnpm-store 8 | 9 | # Local env files 10 | .env 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # Testing 17 | coverage 18 | 19 | # Turbo 20 | .turbo 21 | 22 | # Vercel 23 | .vercel 24 | 25 | # Build Outputs 26 | .next/ 27 | out/ 28 | build 29 | dist 30 | 31 | 32 | # Debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Misc 38 | .DS_Store 39 | *.pem 40 | /.idea 41 | 42 | # Video converter folders 43 | video-converter 44 | rename 45 | 46 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | store-dir=.pnpm-store 2 | package-import-method=copy -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false, 4 | "bracketSpacing": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Next.js: debug full stack", 5 | "type": "node", 6 | "request": "launch", 7 | "program": "${workspaceFolder}/apps/web/node_modules/next/dist/bin/next", 8 | "runtimeArgs": ["--inspect"], 9 | "skipFiles": ["/**"], 10 | "serverReadyAction": { 11 | "action": "debugWithChrome", 12 | "killOnServerStop": true, 13 | "pattern": "- Local:.+(https?://.+)", 14 | "uriFormat": "%s", 15 | "webRoot": "${workspaceFolder}" 16 | }, 17 | "cwd": "${workspaceFolder}/apps/web" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": false, 3 | "prettier.configPath": "./.prettierrc" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 21st.dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/backend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serafimcloud/21st/b96d84dcf748d5e56f2f72f2bab9a4f7f33574cf/apps/backend/bun.lockb -------------------------------------------------------------------------------- /apps/backend/nixpacks.toml: -------------------------------------------------------------------------------- 1 | [phases.setup] 2 | aptPkgs = ["...", "ffmpeg"] -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "bun run --watch serve.ts", 8 | "start": "bun run serve.ts" 9 | }, 10 | "dependencies": { 11 | "@aws-sdk/client-s3": "^3.758.0", 12 | "@babel/preset-react": "^7.26.3", 13 | "@babel/preset-typescript": "^7.26.0", 14 | "@vitejs/plugin-react": "^4.3.4", 15 | "endent": "^2.1.0", 16 | "esbuild": "^0.25.0", 17 | "lodash": "^4.17.21", 18 | "postcss": "^8.4.35", 19 | "react": "^19.0.0", 20 | "react-dom": "19.0.0", 21 | "tailwindcss": "^3.4.1", 22 | "typescript": "^5.3.3", 23 | "vite": "^6.2.1", 24 | "vite-plugin-singlefile": "^2.1.0" 25 | }, 26 | "devDependencies": { 27 | "@types/bun": "^1.1.14", 28 | "@types/lodash": "^4.14.202" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/backend/serve.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from "./src/server" 2 | 3 | console.log("HEY MAN", process.env.PORT) 4 | console.log("r2 access key", process.env.R2_ACCESS_KEY_ID) 5 | startServer(process.env.PORT ? parseInt(process.env.PORT) : 80) 6 | -------------------------------------------------------------------------------- /apps/backend/src/bundler/types.ts: -------------------------------------------------------------------------------- 1 | export interface BundleOptions { 2 | files: Record // path -> content 3 | dependencies?: Record // package name -> version 4 | tailwindConfig?: string 5 | globalCss?: string 6 | bundledCss?: string 7 | } 8 | 9 | export interface BundleResult { 10 | js: string 11 | css: string 12 | html?: string 13 | bundler: "vite" 14 | } 15 | -------------------------------------------------------------------------------- /apps/backend/src/bundler/vite.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises" 2 | import path from "path" 3 | 4 | export const bundleWithVite = async ( 5 | tempDir: string, 6 | outDir: string, 7 | ): Promise => { 8 | try { 9 | console.log("vite: Starting bundling process...") 10 | 11 | await fs.writeFile( 12 | path.join(tempDir, "vite.config.js"), 13 | ` 14 | import { defineConfig } from 'vite'; 15 | import react from '@vitejs/plugin-react'; 16 | import path from 'path'; 17 | import { viteSingleFile } from 'vite-plugin-singlefile'; 18 | 19 | export default defineConfig({ 20 | plugins: [ 21 | react(), 22 | viteSingleFile() 23 | ], 24 | resolve: { 25 | alias: { 26 | '@': path.resolve(__dirname, './src'), 27 | 'next': path.resolve(__dirname, 'node_modules/next'), 28 | }, 29 | extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'] 30 | }, 31 | build: { 32 | outDir: '${path.relative(tempDir, outDir)}', 33 | sourcemap: false, 34 | minify: true, 35 | }, 36 | }); 37 | `, 38 | ) 39 | 40 | const { build } = await import("vite") 41 | 42 | await build({ 43 | root: tempDir, 44 | configFile: path.join(tempDir, "vite.config.js"), 45 | logLevel: "info", 46 | }) 47 | 48 | console.log("vite: Bundle completed successfully") 49 | return true 50 | } catch (error) { 51 | console.warn("vite: Bundling error:", error) 52 | return false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/backend/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "bun" 2 | import { setupRoutes } from "../routes" 3 | 4 | export const startServer = (port: number) => { 5 | const server = serve({ 6 | port, 7 | fetch: (req) => setupRoutes(req), 8 | }) 9 | 10 | console.log(`Server running at http://localhost:${server.port}`) 11 | 12 | return server 13 | } 14 | -------------------------------------------------------------------------------- /apps/backend/src/video-converter.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process" 2 | import fs from "fs/promises" 3 | import path from "path" 4 | import os from "os" 5 | 6 | export function convertVideo( 7 | inputPath: string, 8 | outputPath: string, 9 | ): Promise { 10 | return new Promise((resolve, reject) => { 11 | const command = `ffmpeg -i "${inputPath}" -c:v libx264 -crf 23 -preset medium -an "${outputPath}"` 12 | exec(command, (error) => { 13 | if (error) { 14 | reject(error) 15 | return 16 | } 17 | resolve() 18 | }) 19 | }) 20 | } 21 | 22 | export async function handleVideoConversion( 23 | file: File, 24 | ): Promise<{ video: Buffer; filename: string }> { 25 | const tempDir = path.join(os.tmpdir(), "video-conversions") 26 | await fs.mkdir(tempDir, { recursive: true }) 27 | 28 | const sanitizedFilename = file.name.replace(/[^a-zA-Z0-9.-]/g, "_") 29 | const tempInputPath = path.join(tempDir, sanitizedFilename) 30 | const tempOutputPath = path.join( 31 | tempDir, 32 | sanitizedFilename.replace(/\.[^/.]+$/, "") + `_converted_${Date.now()}.mp4`, 33 | ) 34 | 35 | const bytes = await file.arrayBuffer() 36 | await fs.writeFile(tempInputPath, Buffer.from(bytes)) 37 | 38 | await convertVideo(tempInputPath, tempOutputPath) 39 | const processedVideo = await fs.readFile(tempOutputPath) 40 | 41 | await Promise.all([fs.unlink(tempInputPath), fs.unlink(tempOutputPath)]) 42 | 43 | return { 44 | video: processedVideo, 45 | filename: path.basename(tempOutputPath), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | rules: { 10 | "@next/next/no-img-element": "off", 11 | }, 12 | overrides: [ 13 | { 14 | files: ["tailwind.config.js"], 15 | parser: "espree", 16 | parserOptions: { 17 | ecmaVersion: 2020, 18 | }, 19 | }, 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files (can opt-in for commiting if needed) 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # css 39 | css/* 40 | .env*.local 41 | 42 | /prisma/client -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/web/app/SessionRecorder.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { usePathname } from "next/navigation" 3 | import { useEffect } from "react" 4 | import posthog from "posthog-js" 5 | import { initPostHog } from "@/lib/posthog" 6 | 7 | const RECORDED_ROUTES = ["/studio", "/publish"] 8 | 9 | export default function SessionRecorder() { 10 | const pathname = usePathname() 11 | 12 | useEffect(initPostHog, []) 13 | 14 | useEffect(() => { 15 | try { 16 | const shouldRecord = RECORDED_ROUTES.some((route) => 17 | pathname.startsWith(route), 18 | ) 19 | 20 | if (shouldRecord) { 21 | posthog.startSessionRecording() 22 | } else { 23 | posthog.stopSessionRecording() 24 | } 25 | 26 | posthog.capture("$pageview", { url: pathname }) 27 | } catch (error) { 28 | console.error("Error recording session", error) 29 | } 30 | }, [pathname]) 31 | 32 | return null 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/app/[username]/[component_slug]/[demo_slug]/page.tsx: -------------------------------------------------------------------------------- 1 | export { /* @next-codemod-error `default` export is re-exported. Check if this component uses `params` or `searchParams`*/ 2 | default } from "../page" 3 | -------------------------------------------------------------------------------- /apps/web/app/[username]/[component_slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingSpinnerPage } from "@/components/ui/loading-spinner" 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/api/cron/gen-usage-embeddings/route.ts: -------------------------------------------------------------------------------- 1 | import { supabaseWithAdminAccess } from "@/lib/supabase" 2 | import { NextRequest, NextResponse } from "next/server" 3 | 4 | export const maxDuration = 600 5 | 6 | export async function GET(req: NextRequest): Promise { 7 | // Validate the request has the correct authorization header 8 | const authHeader = req.headers.get("Authorization") 9 | if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { 10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 11 | } 12 | 13 | const supabase = supabaseWithAdminAccess 14 | 15 | const { data: missingItems, error } = await supabase.rpc( 16 | "get_missing_usage_embedding_items", 17 | ) 18 | 19 | if (error) { 20 | console.error("Error fetching missing items:", error) 21 | return NextResponse.json( 22 | { error: "Failed to fetch missing items", details: error.message }, 23 | { status: 500 }, 24 | ) 25 | } 26 | 27 | // Generate embeddings for missing items 28 | for (const item of missingItems) { 29 | console.log("Generating embeddings for item:", item) 30 | const { data, error } = await supabase.functions.invoke( 31 | "generate-embeddings", 32 | { 33 | body: { 34 | type: item.item_type, 35 | id: item.item_id, 36 | }, 37 | }, 38 | ) 39 | 40 | console.log("Response:", data) 41 | 42 | if (error) { 43 | console.error("Error generating embeddings:", error) 44 | return NextResponse.json( 45 | { error: "Failed to generate embeddings", details: error.message }, 46 | { status: 500 }, 47 | ) 48 | } 49 | } 50 | 51 | return NextResponse.json(missingItems) 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/app/api/emails/submission-status/route.ts: -------------------------------------------------------------------------------- 1 | import { sendSubmissionStatusEmail } from "@/lib/emails/send-submission-status" 2 | import { Submission, SubmissionStatus } from "@/components/features/admin/types" 3 | import { NextResponse } from "next/server" 4 | 5 | export async function POST(request: Request) { 6 | try { 7 | const body = await request.json() 8 | const { submission, status, feedback } = body as { 9 | submission: Submission 10 | status: SubmissionStatus 11 | feedback?: string 12 | } 13 | 14 | if (!submission || !status) { 15 | return NextResponse.json( 16 | { success: false, error: "Missing required fields" }, 17 | { status: 400 }, 18 | ) 19 | } 20 | 21 | const result = await sendSubmissionStatusEmail({ 22 | submission, 23 | status, 24 | feedback, 25 | }) 26 | 27 | if (result.success) { 28 | return NextResponse.json({ success: true, data: result.data }) 29 | } else { 30 | return NextResponse.json( 31 | { success: false, error: result.error }, 32 | { status: 500 }, 33 | ) 34 | } 35 | } catch (error) { 36 | console.error("Error sending submission status email:", error) 37 | return NextResponse.json( 38 | { 39 | success: false, 40 | error: error instanceof Error ? error.message : "Unknown error", 41 | }, 42 | { status: 500 }, 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/app/api/sandbox/publish/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server" 2 | import { NextResponse } from "next/server" 3 | import { supabaseWithAdminAccess } from "@/lib/supabase" 4 | import ShortUUID from "short-uuid" 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const { userId } = await auth() 9 | if (!userId) { 10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 11 | } 12 | 13 | const { shortSandboxId } = await request.json() 14 | 15 | const sandboxId = ShortUUID().toUUID(shortSandboxId) 16 | 17 | if (!sandboxId) { 18 | return NextResponse.json( 19 | { error: "Sandbox ID is required" }, 20 | { status: 400 }, 21 | ) 22 | } 23 | 24 | const { data: sandbox, error } = await supabaseWithAdminAccess 25 | .from("sandboxes") 26 | .select("codesandbox_id") 27 | .eq("id", sandboxId) 28 | .eq("user_id", userId) 29 | .single() 30 | 31 | if (error || !sandbox) { 32 | return NextResponse.json( 33 | { error: "Sandbox not found or access denied" }, 34 | { status: 404 }, 35 | ) 36 | } 37 | 38 | const { error: updateError } = await supabaseWithAdminAccess 39 | .from("sandboxes") 40 | .update({ status: "on_review" }) 41 | .eq("id", sandboxId) 42 | .eq("user_id", userId) 43 | 44 | if (updateError) { 45 | return NextResponse.json( 46 | { error: "Failed to update sandbox status" }, 47 | { status: 500 }, 48 | ) 49 | } 50 | 51 | return NextResponse.json({ success: true }) 52 | } catch (error) { 53 | console.error("Error connecting to sandbox:", error) 54 | return NextResponse.json( 55 | { error: "Internal Server Error" }, 56 | { status: 500 }, 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/app/api/stripe/account/route.ts: -------------------------------------------------------------------------------- 1 | import stripe, { getStripeId } from "@/lib/stripe" 2 | import { auth } from "@clerk/nextjs/server" 3 | import { NextRequest, NextResponse } from "next/server" 4 | 5 | export async function GET(request: NextRequest) { 6 | try { 7 | const { userId } = await auth() 8 | if (!userId) { 9 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 10 | } 11 | 12 | const stripeId = await getStripeId(userId) 13 | const account = await stripe.accounts.retrieve(stripeId) 14 | 15 | return NextResponse.json(account) 16 | } catch (error) { 17 | return NextResponse.json( 18 | { error: `Failed to get stripe account` }, 19 | { status: 500 }, 20 | ) 21 | } 22 | } 23 | 24 | export async function POST(request: NextRequest) { 25 | try { 26 | const { userId } = await auth() 27 | if (!userId) { 28 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 29 | } 30 | 31 | const stripeId = await getStripeId(userId) 32 | 33 | return NextResponse.json({ stripeId }) 34 | } catch (error) { 35 | return NextResponse.json( 36 | { error: `Failed to get stripe account` }, 37 | { status: 500 }, 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/api/stripe/get-account-link/route.ts: -------------------------------------------------------------------------------- 1 | import stripe, { getStripeId } from "@/lib/stripe" 2 | import { supabaseWithAdminAccess } from "@/lib/supabase" 3 | import { auth } from "@clerk/nextjs/server" 4 | import { NextRequest, NextResponse } from "next/server" 5 | 6 | export async function GET(request: NextRequest) { 7 | try { 8 | const { userId } = await auth() 9 | if (!userId) { 10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 11 | } 12 | 13 | const { data: userData, error: userError } = await supabaseWithAdminAccess 14 | .from("users") 15 | .select("*") 16 | .eq("id", userId) 17 | .single() 18 | 19 | if (userError) { 20 | return NextResponse.json( 21 | { error: "Failed to get user data" }, 22 | { status: 500 }, 23 | ) 24 | } 25 | 26 | const stripeId = await getStripeId(userId) 27 | const returnUrl = `${process.env.NEXT_PUBLIC_APP_URL}/studio/${userData.display_username}/monetization` 28 | 29 | const accountLink = await stripe.accountLinks.create({ 30 | account: stripeId, 31 | refresh_url: returnUrl, 32 | return_url: returnUrl, 33 | type: "account_onboarding", 34 | }) 35 | 36 | return NextResponse.json({ url: accountLink.url }) 37 | } catch (error) { 38 | return NextResponse.json( 39 | { error: "Failed to get stripe account" }, 40 | { status: 500 }, 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/app/api/stripe/get-dashboard-link/route.ts: -------------------------------------------------------------------------------- 1 | import stripe, { getStripeId } from "@/lib/stripe" 2 | import { auth } from "@clerk/nextjs/server" 3 | import { NextRequest, NextResponse } from "next/server" 4 | 5 | export async function GET(request: NextRequest) { 6 | try { 7 | const { userId } = await auth() 8 | if (!userId) { 9 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 10 | } 11 | 12 | const stripeId = await getStripeId(userId) 13 | 14 | const accountLink = await stripe.accounts.createLoginLink(stripeId) 15 | 16 | return NextResponse.json({ url: accountLink.url }) 17 | } catch (error) { 18 | return NextResponse.json( 19 | { error: "Failed to get stripe account" }, 20 | { status: 500 }, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/api/svgl/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | export async function GET(request: Request) { 4 | const { searchParams } = new URL(request.url) 5 | const category = searchParams.get("category") 6 | const search = searchParams.get("search") 7 | const type = searchParams.get("type") 8 | const svgUrl = searchParams.get("svg") 9 | 10 | if (svgUrl) { 11 | try { 12 | const response = await fetch(svgUrl) 13 | const svgText = await response.text() 14 | return new NextResponse(svgText, { 15 | headers: { 16 | "Content-Type": "image/svg+xml", 17 | }, 18 | }) 19 | } catch (error) { 20 | return NextResponse.json( 21 | { error: "Failed to fetch SVG content" }, 22 | { status: 500 }, 23 | ) 24 | } 25 | } 26 | 27 | let url = "https://api.svgl.app" 28 | 29 | if (type === "categories") { 30 | url += "/categories" 31 | } else { 32 | if (category && category !== "all") { 33 | const formattedCategory = category.toLowerCase() 34 | url = `https://api.svgl.app/category/${formattedCategory}` 35 | } 36 | if (search) { 37 | const searchParams = new URLSearchParams({ search }) 38 | url += `?${searchParams.toString()}` 39 | } 40 | } 41 | 42 | try { 43 | const response = await fetch(url, { 44 | headers: { 45 | Accept: "application/json", 46 | }, 47 | }) 48 | 49 | if (!response.ok) { 50 | throw new Error(`Failed to fetch from SVGL API: ${response.statusText}`) 51 | } 52 | 53 | const data = await response.json() 54 | return NextResponse.json(data) 55 | } catch (error) { 56 | console.error("SVGL API Error:", error) 57 | return NextResponse.json( 58 | { error: "Failed to fetch from SVGL API" }, 59 | { status: 500 }, 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/app/api/user/delete-account/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { auth } from "@clerk/nextjs/server" 3 | import { supabaseWithAdminAccess } from "@/lib/supabase" 4 | 5 | export async function DELETE() { 6 | try { 7 | const { userId } = await auth() 8 | 9 | if (!userId) { 10 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 11 | } 12 | 13 | // Delete user from Supabase database 14 | const { error } = await supabaseWithAdminAccess 15 | .from("users") 16 | .delete() 17 | .eq("id", userId) 18 | 19 | if (error) { 20 | console.error("Error deleting user account from Supabase:", error) 21 | return NextResponse.json( 22 | { error: "Failed to delete account from database" }, 23 | { status: 500 }, 24 | ) 25 | } 26 | 27 | // Note: This only deletes the user from the Supabase database 28 | // The Clerk account deletion will be handled by the webhook when the user deletes their account 29 | // through the Clerk User Portal 30 | 31 | return NextResponse.json({ 32 | success: true, 33 | message: 34 | "Account data deleted. Please visit your account settings to delete your authentication account.", 35 | clerkAccountUrl: 36 | process.env.NODE_ENV === "development" 37 | ? "https://wanted-titmouse-48.accounts.dev/user" 38 | : "https://accounts.21st.dev/user", 39 | }) 40 | } catch (error) { 41 | console.error("Error in delete account API:", error) 42 | return NextResponse.json( 43 | { error: "Internal server error" }, 44 | { status: 500 }, 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/app/api/user/profile/check-username/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server" 2 | import { NextResponse } from "next/server" 3 | import { createClient } from "@supabase/supabase-js" 4 | 5 | const supabaseAdmin = createClient( 6 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 7 | process.env.SUPABASE_SERVICE_ROLE_KEY!, 8 | { 9 | auth: { 10 | persistSession: false, 11 | }, 12 | }, 13 | ) 14 | 15 | export async function POST(req: Request) { 16 | try { 17 | const session = await auth() 18 | const userId = session?.userId 19 | if (!userId) { 20 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) 21 | } 22 | 23 | const body = await req.json() 24 | const { display_username } = body 25 | 26 | if (!display_username) { 27 | return NextResponse.json( 28 | { error: "Username is required" }, 29 | { status: 400 }, 30 | ) 31 | } 32 | 33 | // Check if display_username is unique across both username and display_username fields 34 | const { data: existingUsers, error: queryError } = await supabaseAdmin 35 | .from("users") 36 | .select("id") 37 | .or( 38 | `username.eq."${display_username}",display_username.eq."${display_username}"`, 39 | ) 40 | .neq("id", userId) 41 | 42 | if (queryError) { 43 | console.error("Username validation error:", queryError) 44 | return NextResponse.json( 45 | { error: "Failed to validate username" }, 46 | { status: 500 }, 47 | ) 48 | } 49 | 50 | return NextResponse.json({ 51 | exists: existingUsers && existingUsers.length > 0, 52 | }) 53 | } catch (error) { 54 | console.error("Error checking username:", error) 55 | return NextResponse.json( 56 | { error: "Internal server error" }, 57 | { status: 500 }, 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/app/c/[collection_slug]/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useAtom } from "jotai" 4 | import { ComponentsList } from "@/components/ui/items-list" 5 | import { sortByAtom } from "@/components/features/main-page/main-page-header" 6 | import { CollectionWithUser, SortOption } from "@/types/global" 7 | import { useLayoutEffect } from "react" 8 | import { CollectionHeader } from "@/components/features/collections/collection-header" 9 | 10 | export function CollectionPageContent({ 11 | initialSortBy, 12 | collection, 13 | }: { 14 | initialSortBy: SortOption 15 | collection: CollectionWithUser 16 | }) { 17 | const [sortBy, setSortBy] = useAtom(sortByAtom) 18 | 19 | useLayoutEffect(() => { 20 | if (sortBy === undefined) setSortBy(initialSortBy) 21 | }, [sortBy, setSortBy, initialSortBy]) 22 | 23 | console.log("[CollectionPageContent] Collection data:", { 24 | id: collection.id, 25 | name: collection.name, 26 | sortBy: sortBy || initialSortBy, 27 | }) 28 | 29 | return ( 30 |
31 | 32 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/import-old/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/nextjs" 2 | import { Metadata } from "next" 3 | import { Header } from "@/components/ui/header.client" 4 | import ImportPageClient from "./page.client" 5 | 6 | export const metadata: Metadata = { 7 | title: "Import Component | 21st.dev", 8 | } 9 | 10 | export default function ImportPage() { 11 | return ( 12 | <> 13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/magic-chat/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { MagicChatPageClient } from "./page.client" 3 | 4 | export const metadata: Metadata = { 5 | title: "Magic Chat - Create Standout Components with AI | 21st.dev", 6 | description: 7 | "Start with a prompt, iterate in chat, and draw inspiration from the best works of 21st.dev's top design engineers. Create standout UI components with AI-powered Magic Chat.", 8 | keywords: [ 9 | "Magic Chat", 10 | "AI UI generation", 11 | "standout components", 12 | "UI components", 13 | "React components", 14 | "TypeScript", 15 | "Next.js", 16 | "21st.dev", 17 | "AI chat", 18 | "component generation", 19 | "design inspiration", 20 | "design engineers", 21 | "iterate in chat", 22 | "prompt-based design", 23 | "modern component patterns", 24 | ], 25 | openGraph: { 26 | title: "Magic Chat - Create Standout Components with AI | 21st.dev", 27 | description: 28 | "Start with a prompt, iterate in chat, and draw inspiration from the best works of 21st.dev's top design engineers. Create standout UI components with AI-powered Magic Chat.", 29 | images: ["/magic-chat-og.png"], 30 | type: "website", 31 | siteName: "21st.dev", 32 | locale: "en_US", 33 | }, 34 | twitter: { 35 | card: "summary_large_image", 36 | title: "Magic Chat - Create Standout Components with AI | 21st.dev", 37 | description: 38 | "Start with a prompt, iterate in chat, and draw inspiration from the best works of 21st.dev's top design engineers. Create standout UI components with AI-powered Magic Chat.", 39 | images: ["/magic-chat-og.png"], 40 | }, 41 | } 42 | 43 | export default function MagicChatPage() { 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/app/magic/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next/types" 2 | 3 | export const metadata: Metadata = { 4 | title: "Magic AI Agent", 5 | description: "AI Agent for Your IDE That Creates Professional UI Components", 6 | } 7 | 8 | export default function MagicLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return <>{children} 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/magic/onboarding/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server" 2 | import { supabaseWithAdminAccess } from "@/lib/supabase" 3 | import { OnboardingClient } from "./page.client" 4 | 5 | async function getApiKey(userId: string) { 6 | const supabase = supabaseWithAdminAccess 7 | const { data: rawApiKey } = await supabase 8 | .from("api_keys") 9 | .select("*") 10 | .eq("user_id", userId) 11 | .eq("is_active", true) 12 | .single() 13 | 14 | if (!rawApiKey) return null 15 | 16 | return { 17 | id: rawApiKey.id, 18 | key: rawApiKey.key, 19 | user_id: rawApiKey.user_id, 20 | plan: rawApiKey.plan || "free", 21 | requests_limit: rawApiKey.requests_limit || 100, 22 | requests_count: rawApiKey.requests_count || 0, 23 | created_at: rawApiKey.created_at || new Date().toISOString(), 24 | expires_at: rawApiKey.expires_at, 25 | last_used_at: rawApiKey.last_used_at, 26 | is_active: rawApiKey.is_active ?? true, 27 | project_url: rawApiKey.project_url || "https://21st.dev/magic", 28 | } 29 | } 30 | 31 | export default async function OnboardingPage() { 32 | const { userId } = await auth() 33 | const apiKey = userId ? await getApiKey(userId) : null 34 | 35 | return ( 36 |
37 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/app/magic/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect, useRef } from "react" 4 | import { Hero } from "@/components/features/magic/hero" 5 | import { HowItWorks } from "@/components/features/magic/how-it-works" 6 | import { FAQ } from "@/components/features/magic/faq" 7 | import { Footer } from "@/components/ui/footer" 8 | import { SupportedEditors } from "@/components/features/magic/supported-editors" 9 | import { Features } from "@/components/features/magic/features" 10 | import { MagicHeader } from "@/components/features/magic/magic-header" 11 | 12 | export function MagicPageClient() { 13 | const [isScrolled, setIsScrolled] = useState(false) 14 | const scrollRef = useRef(null) 15 | 16 | useEffect(() => { 17 | const scrollElement = scrollRef.current 18 | 19 | const handleScroll = () => { 20 | if (scrollElement) { 21 | setIsScrolled(scrollElement.scrollTop > 30) 22 | } 23 | } 24 | 25 | if (scrollElement) { 26 | scrollElement.addEventListener("scroll", handleScroll) 27 | handleScroll() 28 | 29 | return () => { 30 | scrollElement.removeEventListener("scroll", handleScroll) 31 | } 32 | } 33 | }, []) 34 | 35 | return ( 36 |
40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/app/maintenance/page.tsx: -------------------------------------------------------------------------------- 1 | import { MaintenancePage } from '@/components/ui/maintenance-page'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | 7 | export const metadata = { 8 | title: 'Under Maintenance | Back Soon', 9 | description: 'Our site is under maintenance. We will be back in an hour!', 10 | } -------------------------------------------------------------------------------- /apps/web/app/manifest.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next" 2 | import { SITE_NAME, SITE_SLOGAN } from "@/lib/constants" 3 | export default function manifest(): MetadataRoute.Manifest { 4 | return { 5 | name: `${SITE_NAME} - ${SITE_SLOGAN}`, 6 | short_name: `${SITE_NAME}`, 7 | description: 8 | "Ship polished UIs faster with ready-to-use React Tailwind components inspired by shadcn/ui.", 9 | start_url: "/", 10 | display: "standalone", 11 | background_color: "#FFFFFF", 12 | theme_color: "#09090B", 13 | icons: [ 14 | { 15 | src: "/icon.png", 16 | sizes: "192x192", 17 | type: "image/png", 18 | }, 19 | ], 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/pricing/page.tsx: -------------------------------------------------------------------------------- 1 | import { Pricing } from "@/app/pricing/page.client" 2 | import { Header } from "@/components/ui/header.client" 3 | import { Footer } from "@/components/ui/footer" 4 | import { Suspense } from "react" 5 | 6 | export const metadata = { 7 | title: "Pricing - 21st.dev", 8 | description: "Choose the plan that best fits your needs", 9 | } 10 | 11 | export default async function PricingPage() { 12 | return ( 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, Suspense } from "react" 4 | 5 | import { ClerkProvider } from "@clerk/nextjs" 6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query" 7 | 8 | import { CommandMenu } from "@/components/ui/command-menu" 9 | import { SidebarProvider } from "@/components/ui/sidebar" 10 | import { MainSidebar } from "@/components/features/main-page/sidebar-layout" 11 | import { MainLayout } from "@/components/features/main-page/main-layout" 12 | import { useSidebarVisibility } from "@/hooks/use-sidebar-visibility" 13 | 14 | import { initAmplitude } from "@/lib/amplitude" 15 | import { useAtom } from "jotai" 16 | import { sidebarOpenAtom } from "@/components/features/main-page/main-layout" 17 | 18 | const queryClient = new QueryClient() 19 | 20 | export function AppProviders({ 21 | children, 22 | }: { 23 | children: React.ReactNode 24 | }): React.ReactElement { 25 | const [open, setOpen] = useAtom(sidebarOpenAtom) 26 | const shouldShowSidebar = useSidebarVisibility() 27 | 28 | useEffect(() => { 29 | initAmplitude() 30 | }, []) 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | {shouldShowSidebar && } 38 | 39 | 40 | 41 | {children} 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/app/public-dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { PublicDashboardClient } from "./page.client" 3 | 4 | export const metadata: Metadata = { 5 | title: "21st.dev - Public Payouts Dashboard", 6 | description: "View all authors receiving payouts in 21st.dev", 7 | } 8 | 9 | export default function PublicDashboardPage() { 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/publish/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/nextjs" 3 | import PublishComponentForm from "@/components/features/publish/publish-layout" 4 | import { Metadata } from "next" 5 | 6 | import { Header } from "@/components/ui/header.client" 7 | 8 | export const metadata: Metadata = { 9 | title: "Publish New Component | 21st.dev", 10 | } 11 | 12 | export default function PublishPage() { 13 | return ( 14 | <> 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/app/publish/template/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/nextjs" 3 | import { PublishTemplateForm } from "@/components/features/publish/template/publish-template-form" 4 | import { Metadata } from "next" 5 | import { Header } from "@/components/ui/header.client" 6 | 7 | export const metadata: Metadata = { 8 | title: "Publish New Template | 21st.dev", 9 | description: "Create and publish a new template", 10 | } 11 | 12 | export default function PublishTemplatePage() { 13 | return ( 14 | <> 15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/q/[query]/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useEffect } from "react" 4 | import { useAtom } from "jotai" 5 | import { motion } from "motion/react" 6 | import { searchQueryAtom } from "@/components/ui/header.client" 7 | import { sortByAtom } from "@/components/features/main-page/main-page-header" 8 | import { SortOption } from "@/types/global" 9 | import ComponentsList from "@/components/ui/items-list" 10 | 11 | type SearchPageClientProps = { 12 | initialQuery: string 13 | initialSortBy: SortOption 14 | } 15 | 16 | export function SearchPageClient({ 17 | initialQuery, 18 | initialSortBy, 19 | }: SearchPageClientProps) { 20 | const [, setSearchQuery] = useAtom(searchQueryAtom) 21 | const [sortBy, setSortBy] = useAtom(sortByAtom) 22 | 23 | useEffect(() => { 24 | setSearchQuery(initialQuery) 25 | setSortBy(initialSortBy) 26 | }, [initialQuery, initialSortBy, setSearchQuery, setSortBy]) 27 | 28 | return ( 29 | 34 |
35 |
36 |

37 | Search results for "{initialQuery}" 38 |

39 |
40 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/app/s/[tag_slug]/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useAtom } from "jotai" 4 | import { ComponentsList } from "@/components/ui/items-list" 5 | import { sortByAtom } from "@/components/features/main-page/main-page-header" 6 | import { SortOption } from "@/types/global" 7 | import { TagComponentsHeader } from "@/components/features/tag-page/tag-page-header" 8 | import { useLayoutEffect } from "react" 9 | import { motion } from "motion/react" 10 | 11 | export function TagPageContent({ 12 | tagName, 13 | tagSlug, 14 | initialSortBy, 15 | }: { 16 | tagName: string 17 | tagSlug: string 18 | initialSortBy: SortOption 19 | }) { 20 | const [sortBy, setSortBy] = useAtom(sortByAtom) 21 | 22 | useLayoutEffect(() => { 23 | if (sortBy === undefined) setSortBy(initialSortBy) 24 | }, []) 25 | 26 | return ( 27 |
28 |
29 | 30 | 38 | 43 | 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/app/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next/types" 2 | import { SettingsMobileNav } from "@/components/features/settings/settings-mobile-nav" 3 | import { SettingsSidebar } from "@/components/features/settings/settings-sidebar" 4 | 5 | interface SettingsLayoutProps { 6 | children: React.ReactNode 7 | } 8 | 9 | export const metadata: Metadata = { 10 | title: "Settings", 11 | description: "Manage your account settings and preferences.", 12 | } 13 | 14 | export default function SettingsLayout({ children }: SettingsLayoutProps) { 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 | 26 |
27 | {children} 28 |
29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | export default function SettingsPage() { 4 | redirect("/settings/profile") 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/settings/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import ProfileSettingsPage from "./page.client" 3 | 4 | export const metadata: Metadata = { 5 | title: "Profile Settings", 6 | description: "Manage your profile settings and preferences.", 7 | } 8 | 9 | export default function SettingsProfilePage() { 10 | return ( 11 | <> 12 |
13 |
14 |

Profile

15 |

16 | Manage how others see you on the platform 17 |

18 |
19 |
20 | 21 | 22 | ) 23 | } 24 | 25 | -------------------------------------------------------------------------------- /apps/web/app/settings/rules/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { redirect } from "next/navigation" 3 | import { auth } from "@clerk/nextjs/server" 4 | import { Button } from "@/components/ui/button" 5 | import { ArrowLeft } from "lucide-react" 6 | import Link from "next/link" 7 | 8 | import { PromptRuleForm } from "@/components/features/prompt-rules/prompt-rule-form" 9 | 10 | export const metadata: Metadata = { 11 | title: "Create AI Rule", 12 | description: "Create a new AI rule for your prompts", 13 | } 14 | 15 | export default async function NewPromptRulePage() { 16 | const { userId } = await auth() 17 | 18 | if (!userId) { 19 | redirect("/sign-in") 20 | } 21 | 22 | return ( 23 |
24 |
25 | 26 | 30 | 31 |
32 | 33 |
34 |

Create AI Rule

35 |

36 | Create a new rule to customize AI prompts with your project context 37 |

38 |
39 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/app/settings/rules/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import Link from "next/link" 3 | import { redirect } from "next/navigation" 4 | import { auth } from "@clerk/nextjs/server" 5 | 6 | import { Button } from "@/components/ui/button" 7 | import { PromptRulesList } from "@/components/features/prompt-rules/prompt-rules-list" 8 | import { getPromptRules } from "@/lib/queries" 9 | import { supabaseWithAdminAccess } from "@/lib/supabase" 10 | 11 | export const metadata: Metadata = { 12 | title: "AI Rules", 13 | description: "Manage your AI prompt rules and context", 14 | } 15 | 16 | export default async function PromptRulesPage() { 17 | const { userId } = await auth() 18 | 19 | if (!userId) { 20 | redirect("/sign-in") 21 | } 22 | 23 | const promptRules = await getPromptRules(supabaseWithAdminAccess, userId) 24 | 25 | return ( 26 |
27 |
28 |
29 |

AI Rules

30 |

31 | For questions about AI rules,{" "} 32 | 36 | contact us 37 | 38 |

39 |
40 | 43 |
44 | 45 | 46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next" 2 | import { supabaseWithAdminAccess } from "@/lib/supabase" 3 | 4 | export default async function sitemap(): Promise { 5 | const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://21st.dev" 6 | 7 | const { data: components } = await supabaseWithAdminAccess 8 | .from("components") 9 | .select( 10 | "component_slug, user_id, users!components_user_id_fkey(username), updated_at", 11 | ) 12 | .eq("is_public", true) 13 | 14 | const { data: tags } = await supabaseWithAdminAccess 15 | .from("tags") 16 | .select("slug") 17 | 18 | const { data: users } = await supabaseWithAdminAccess 19 | .from("users") 20 | .select("username, updated_at") 21 | 22 | const componentUrls = (components || []).map((component) => ({ 23 | url: `${baseUrl}/${component.users?.username}/${component.component_slug}`, 24 | lastModified: component.updated_at, 25 | changeFrequency: "weekly" as const, 26 | priority: 0.8, 27 | })) 28 | 29 | const tagUrls = (tags || []).map((tag) => ({ 30 | url: `${baseUrl}/s/${tag.slug}`, 31 | changeFrequency: "weekly" as const, 32 | priority: 0.7, 33 | })) 34 | 35 | const userUrls = (users || []).map((user) => ({ 36 | url: `${baseUrl}/${user.username}`, 37 | lastModified: user.updated_at, 38 | changeFrequency: "weekly" as const, 39 | priority: 0.6, 40 | })) 41 | 42 | return [ 43 | { 44 | url: baseUrl, 45 | lastModified: new Date(), 46 | changeFrequency: "daily", 47 | priority: 1, 48 | }, 49 | { 50 | url: `${baseUrl}/publish`, 51 | changeFrequency: "monthly", 52 | priority: 0.5, 53 | }, 54 | ...componentUrls, 55 | ...tagUrls, 56 | ...userUrls, 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/app/studio/[username]/bundles/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { BundlesLayout } from "@/components/features/bundles/bundles-layout" 4 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 5 | import { User } from "@/types/global" 6 | 7 | export function BundlesClient({ user }: { user: User }) { 8 | return ( 9 | <> 10 |
11 |
12 |

Bundles

13 |

14 | Create and manage your bundles 15 |

16 |
17 |
18 | 19 | Bundles are currently in Beta 20 | 21 | To add new bundles, please contact us on{" "} 22 | 27 | Discord 28 | {" "} 29 | or{" "} 30 | 31 | support@21st.dev 32 | 33 | 34 | 35 |
36 | 37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/studio/[username]/bundles/page.tsx: -------------------------------------------------------------------------------- 1 | import { StudioLayout } from "@/components/features/studio/studio-layout" 2 | import { authUsernameOrRedirect } from "@/lib/user" 3 | import { Metadata } from "next" 4 | import { BundlesClient } from "./page.client" 5 | 6 | export const metadata: Metadata = { 7 | title: "Bundles", 8 | } 9 | 10 | export default async function BundlesPage({ 11 | params, 12 | }: { 13 | params: Promise<{ username: string }> 14 | }) { 15 | const { user } = await authUsernameOrRedirect( 16 | (await params).username, 17 | "/studio", 18 | ) 19 | 20 | return ( 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/app/studio/[username]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { authUsernameOrRedirect } from "@/lib/user" 2 | 3 | export default async function Layout({ 4 | params, 5 | children, 6 | }: { 7 | params: Promise<{ username: string }> 8 | children: React.ReactNode 9 | }) { 10 | await authUsernameOrRedirect((await params).username, "/studio") 11 | return <>{children} 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/studio/[username]/monetization/page.tsx: -------------------------------------------------------------------------------- 1 | import { MonetizationClient } from "./page.client" 2 | import { Metadata } from "next" 3 | import { StudioLayout } from "@/components/features/studio/studio-layout" 4 | import { auth } from "@clerk/nextjs/server" 5 | import { supabaseWithAdminAccess } from "@/lib/supabase" 6 | import { redirect } from "next/navigation" 7 | import { getUserData } from "@/lib/queries" 8 | 9 | export const metadata: Metadata = { 10 | title: "Monetization", 11 | } 12 | 13 | export default async function MonetizationPage({ 14 | params, 15 | }: { 16 | params: Promise<{ username: string }> 17 | }) { 18 | const username = (await params).username 19 | // Verify user is authenticated 20 | const { userId } = await auth() 21 | if (!userId) { 22 | redirect("/sign-in") 23 | } 24 | 25 | // Get user data from Supabase 26 | const { data: user } = await getUserData(supabaseWithAdminAccess, username) 27 | 28 | if (!user) { 29 | console.error("User not found") 30 | redirect("/studio") 31 | } 32 | 33 | // Verify user has access to this page (own profile or admin) 34 | const { data: currentUser } = await supabaseWithAdminAccess 35 | .from("users") 36 | .select("is_admin") 37 | .eq("id", userId) 38 | .single() 39 | 40 | const isAdmin = currentUser?.is_admin || false 41 | const isOwnProfile = userId === user.id 42 | 43 | if (!isAdmin && !isOwnProfile) { 44 | redirect("/studio") 45 | } 46 | 47 | return ( 48 | 49 |
50 | 51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/app/studio/[username]/sandbox/[sandboxId]/publish/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { SandboxHeader } from "@/components/features/studio/sandbox/components/sandbox-header" 4 | import { useSandbox } from "@/components/features/studio/sandbox/hooks/use-sandbox" 5 | import { useParams } from "next/navigation" 6 | import { useRef, useState } from "react" 7 | import PageClient from "./page.client" 8 | 9 | export default function Page() { 10 | const { username, sandboxId } = useParams() as { 11 | username: string 12 | sandboxId: string 13 | } 14 | const [isNextLoading, setIsNextLoading] = useState(false) 15 | 16 | const submitHandlerRef = useRef<(() => void) | null>(null) 17 | 18 | // Fetch sandbox metadata for header 19 | const { serverSandbox, sandboxStatus } = useSandbox({ sandboxId }) 20 | 21 | // const handleSubmitStatusChange = useCallback((isSubmitting: boolean) => { 22 | // setIsNextLoading(isSubmitting) 23 | // }, []) 24 | 25 | const handleSubmit = () => { 26 | if (submitHandlerRef.current) { 27 | submitHandlerRef.current() 28 | } 29 | } 30 | 31 | return ( 32 | <> 33 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/components/features/admin/AddToContestModal.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/components/features/admin/AdminHeader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { motion } from "motion/react" 3 | 4 | interface AdminHeaderProps { 5 | title: string 6 | subtitle?: string 7 | } 8 | 9 | const AdminHeader: FC = ({ title, subtitle }) => { 10 | return ( 11 | 17 |
18 |
19 |

{title}

20 | {subtitle &&

{subtitle}

} 21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default AdminHeader 28 | -------------------------------------------------------------------------------- /apps/web/components/features/admin/NonAdminPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { motion } from "motion/react" 3 | import Link from "next/link" 4 | import { Button } from "@/components/ui/button" 5 | 6 | const NonAdminPlaceholder: FC = () => { 7 | return ( 8 |
9 | 15 |
16 | 23 | 29 | 30 |
31 |

Admin Access Required

32 |

33 | You don't have access to this page. Please contact an administrator if 34 | you believe this is an error. 35 |

36 | 39 |
40 |
41 | ) 42 | } 43 | 44 | export default NonAdminPlaceholder 45 | -------------------------------------------------------------------------------- /apps/web/components/features/admin/SubmissionStatusFilter.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from "@/components/ui/select" 9 | import { Button } from "@/components/ui/button" 10 | 11 | interface SubmissionStatusFilterProps { 12 | value: string 13 | onChange: (value: string) => void 14 | onRefresh: () => void 15 | } 16 | 17 | const SubmissionStatusFilter: FC = ({ 18 | value, 19 | onChange, 20 | onRefresh, 21 | }) => { 22 | return ( 23 |
24 | 36 | 37 |
38 | ) 39 | } 40 | 41 | export default SubmissionStatusFilter 42 | -------------------------------------------------------------------------------- /apps/web/components/features/admin/types.ts: -------------------------------------------------------------------------------- 1 | export type SubmissionStatus = "on_review" | "featured" | "posted" | "rejected" 2 | 3 | export interface Submission { 4 | id: number 5 | name: string 6 | preview_url: string 7 | video_url: string 8 | updated_at: string 9 | demo_slug: string 10 | component_data: { 11 | id: number 12 | name: string 13 | component_slug: string 14 | downloads_count: number 15 | likes_count: number 16 | license: string 17 | registry: string 18 | website_url: string | null 19 | } 20 | user_data: { 21 | id: string 22 | username: string 23 | display_name: string 24 | display_username: string 25 | email?: string 26 | } 27 | component_user_data: any 28 | total_count: number 29 | view_count: number 30 | bookmarks_count: number 31 | bundle_url: { 32 | html: string 33 | } | null 34 | submission_status: string | null 35 | moderators_feedback: string | null 36 | contest_round_id?: number | null 37 | is_public?: boolean 38 | } 39 | 40 | export interface AdminRpcResponse { 41 | success: boolean 42 | error?: string 43 | } 44 | 45 | export interface UpdateSubmissionParams { 46 | p_component_id: number 47 | p_status: SubmissionStatus 48 | p_feedback: string 49 | p_demo_name?: string 50 | p_demo_slug?: string 51 | } 52 | 53 | export interface UpdateDemoParams { 54 | p_component_id: number 55 | p_demo_name: string 56 | p_demo_slug: string 57 | } 58 | -------------------------------------------------------------------------------- /apps/web/components/features/bolt/bolt-banner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { memo } from "react" 4 | 5 | import bg from "./bolt-bg-1.png" 6 | import text from "./bolt-text.png" 7 | 8 | const BoltBannerContent = memo(function BoltBannerContent() { 9 | return ( 10 | 15 |
16 | Bolt Banner 21 | Bolt Banner 26 |
27 |
28 | ) 29 | }) 30 | 31 | export function BoltBanner() { 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/components/features/bolt/bolt-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serafimcloud/21st/b96d84dcf748d5e56f2f72f2bab9a4f7f33574cf/apps/web/components/features/bolt/bolt-bg-1.png -------------------------------------------------------------------------------- /apps/web/components/features/bolt/bolt-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serafimcloud/21st/b96d84dcf748d5e56f2f72f2bab9a4f7f33574cf/apps/web/components/features/bolt/bolt-text.png -------------------------------------------------------------------------------- /apps/web/components/features/categories/category-preview-image.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | interface CategoryPreviewImageProps { 6 | src: string 7 | alt: string 8 | fallbackSrc?: string 9 | className?: string 10 | } 11 | 12 | export function CategoryPreviewImage({ 13 | src, 14 | alt, 15 | fallbackSrc = "/placeholder.svg", 16 | className, 17 | }: CategoryPreviewImageProps) { 18 | const [error, setError] = useState(false) 19 | 20 | return ( 21 | {alt} setError(true)} 28 | /> 29 | ) 30 | } 31 | 32 | export default CategoryPreviewImage 33 | -------------------------------------------------------------------------------- /apps/web/components/features/collections/collection-header.tsx: -------------------------------------------------------------------------------- 1 | import { CollectionWithUser } from "@/types/global" 2 | import { UserAvatar } from "@/components/ui/user-avatar" 3 | import Link from "next/link" 4 | 5 | interface CollectionHeaderProps { 6 | collection: CollectionWithUser 7 | } 8 | 9 | export function CollectionHeader({ collection }: CollectionHeaderProps) { 10 | return ( 11 |
12 |

{collection.name}

13 | {collection.description && ( 14 |

{collection.description}

15 | )} 16 |
17 | Created by 18 |
19 | 23 | 38 | 39 | {collection.user_data?.display_name || 40 | collection.user_data?.name || 41 | "Unknown"} 42 | 43 |
44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/components/features/component-page/component-preview.module.css: -------------------------------------------------------------------------------- 1 | .customScroller :global(.cm-scroller) { 2 | max-height: calc(var(--vh, 1vh) * 100 - 120px) !important; 3 | height: auto !important; 4 | border-radius: 8px !important; 5 | overflow: hidden !important; 6 | } 7 | 8 | .codeViewerWrapper :global(.cm-scroller) { 9 | height: 100% !important; 10 | overflow-y: auto !important; 11 | } 12 | 13 | .codeViewerWrapper { 14 | height: calc(var(--vh, 1vh) * 100 - 200px); 15 | overflow: hidden; 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/components/features/component-page/feature-cards.tsx: -------------------------------------------------------------------------------- 1 | import { Check } from "lucide-react" 2 | 3 | interface FeatureCard { 4 | title: string 5 | description: string 6 | } 7 | 8 | interface FeatureCardsProps { 9 | title: string 10 | features: FeatureCard[] 11 | } 12 | 13 | 14 | export function FeatureCards({ title, features }: FeatureCardsProps) { 15 | return ( 16 |
17 |

{title}

18 |
19 | {features.map((feature, index) => ( 20 |
21 | 22 |
23 | {feature.title} 24 | 25 | {feature.description} 26 | 27 |
28 |
29 | ))} 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/components/features/component-page/new-flow-preview-render.tsx: -------------------------------------------------------------------------------- 1 | import { Demo } from "@/types/global" 2 | import { useTheme } from "next-themes" 3 | import { motion } from "motion/react" 4 | import { FullScreenButton } from "../../ui/full-screen-button" 5 | import { LoadingSpinner } from "../../ui/loading-spinner" 6 | import React, { useState } from "react" 7 | 8 | export function NewFlowPreviewRender({ demo }: { demo: Demo }) { 9 | const { resolvedTheme } = useTheme() 10 | const [isLoading, setIsLoading] = useState(true) 11 | 12 | return ( 13 | 14 | 15 | {isLoading && ( 16 |
17 | 18 |

Loading preview...

19 |
20 | )} 21 |