├── .cursor └── rules │ ├── database-api.mdc │ ├── design-system.mdc │ ├── next-apps.mdc │ ├── package-development.mdc │ ├── project-structure.mdc │ └── testing.mdc ├── .dockerignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-feature-flags.yml │ ├── build-forms.yml │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps ├── docs │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── (home) │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── api │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── content │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── global.css │ │ ├── layout.config.tsx │ │ └── layout.tsx │ ├── components │ │ └── search.tsx │ ├── content │ │ └── docs │ │ │ ├── feature-flags │ │ │ ├── index.mdx │ │ │ ├── meta.json │ │ │ └── sdks │ │ │ │ ├── javascript.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── react.mdx │ │ │ │ └── rest-api.mdx │ │ │ ├── forms │ │ │ ├── features │ │ │ │ ├── meta.json │ │ │ │ └── spam-protection.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── guides │ │ │ │ ├── html.mdx │ │ │ │ ├── javascript.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── react.mdx │ │ │ │ ├── rest.mdx │ │ │ │ └── vue.mdx │ │ │ ├── index.mdx │ │ │ └── meta.json │ │ │ ├── help.mdx │ │ │ ├── meta.json │ │ │ └── self-hosting │ │ │ ├── authentication │ │ │ ├── github-config.mdx │ │ │ ├── google-config.mdx │ │ │ └── meta.json │ │ │ ├── contributing.mdx │ │ │ ├── database │ │ │ ├── meta.json │ │ │ ├── neon.mdx │ │ │ └── supabase.mdx │ │ │ ├── faqs.mdx │ │ │ ├── index.mdx │ │ │ ├── license.mdx │ │ │ ├── meta.json │ │ │ ├── providers │ │ │ ├── deploy-netlify.mdx │ │ │ ├── deploy-render.mdx │ │ │ ├── deploy-vercel.mdx │ │ │ └── meta.json │ │ │ └── updates.mdx │ ├── lib │ │ └── source.ts │ ├── mdx-components.tsx │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── source.config.ts │ └── tsconfig.json ├── feature-flags │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── (authenticated) │ │ │ └── a │ │ │ │ ├── _components │ │ │ │ ├── GetStarted │ │ │ │ │ ├── TextLink.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── RecentProjects │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── Teams │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ └── styles.ts │ │ │ │ ├── invite │ │ │ │ └── [token] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── project │ │ │ │ └── [projectId] │ │ │ │ │ ├── flags │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── FlagsList │ │ │ │ │ │ │ ├── FlagCard │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── FlagRow │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ │ ├── Loading.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── styles.ts │ │ │ │ │ │ └── Toolbar │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── styles.ts │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── settings │ │ │ │ │ ├── environments │ │ │ │ │ ├── _components │ │ │ │ │ │ └── Environments.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── general │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── DeleteProject.tsx │ │ │ │ │ │ ├── Endpoints.tsx │ │ │ │ │ │ ├── Keys.tsx │ │ │ │ │ │ ├── ProjectKey.tsx │ │ │ │ │ │ ├── ProjectName.tsx │ │ │ │ │ │ └── ProjectOwner.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── members │ │ │ │ │ ├── _components │ │ │ │ │ │ └── MembersTable.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── security │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── FlagsIpRules.tsx │ │ │ │ │ │ └── FlagsWebsites.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── styles.ts │ │ │ │ │ └── utils.tsx │ │ │ │ └── user │ │ │ │ └── tab │ │ │ │ ├── billing │ │ │ │ ├── _components │ │ │ │ │ ├── CurrentPlan │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── Meters │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── general │ │ │ │ ├── _components │ │ │ │ │ ├── FlagsCard │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── ThemeCard │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── styles.ts │ │ ├── (unauthenticated) │ │ │ └── auth │ │ │ │ └── sign-in │ │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ ├── trpc │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ └── v1 │ │ │ │ └── [[...route]] │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── not-found.tsx │ ├── components │ │ ├── Activity │ │ │ ├── ActivityList.tsx │ │ │ ├── Toolbar.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── CopyButton │ │ │ └── index.tsx │ │ ├── Navigation │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ └── TooltipIcon │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── i18n │ │ └── request.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── messages │ │ └── en.json │ ├── middleware.ts │ ├── modals │ │ ├── Activity │ │ │ └── index.tsx │ │ ├── Confirm │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Environment │ │ │ ├── Create │ │ │ │ └── index.tsx │ │ │ ├── Update │ │ │ │ └── index.tsx │ │ │ ├── types.ts │ │ │ └── useEnvironmentForm.tsx │ │ ├── Flag │ │ │ ├── Create │ │ │ │ └── index.tsx │ │ │ ├── Tab │ │ │ │ ├── Advance.tsx │ │ │ │ ├── Core.tsx │ │ │ │ └── History.tsx │ │ │ ├── Update │ │ │ │ └── index.tsx │ │ │ ├── styles.ts │ │ │ ├── types.ts │ │ │ └── useFlagForm.tsx │ │ ├── Integration │ │ │ ├── data.js │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Project │ │ │ ├── AddMember │ │ │ │ └── index.tsx │ │ │ └── Create │ │ │ │ └── index.tsx │ │ ├── Team │ │ │ ├── Create │ │ │ │ └── index.tsx │ │ │ └── Manage │ │ │ │ ├── Members │ │ │ │ ├── InviteForm.tsx │ │ │ │ ├── MemberCard.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20220315142123_init │ │ │ │ └── migration.sql │ │ │ ├── 20220316170855_add_password_field_to_user │ │ │ │ └── migration.sql │ │ │ ├── 20220404210013_add_project_table │ │ │ │ └── migration.sql │ │ │ ├── 20220418185341_update_project_fields │ │ │ │ └── migration.sql │ │ │ ├── 20220420162603_environments │ │ │ │ └── migration.sql │ │ │ ├── 20220428161711_flags │ │ │ │ └── migration.sql │ │ │ ├── 20220502201512_history │ │ │ │ └── migration.sql │ │ │ ├── 20220605132228_add_key_to_projects_and_envs │ │ │ │ └── migration.sql │ │ │ ├── 20230129180958_add_default_env_and_user_roles │ │ │ │ └── migration.sql │ │ │ ├── 20230418100546_add_dates_to_project_on_users │ │ │ │ └── migration.sql │ │ │ ├── 20230812173446_update_models_indexes │ │ │ │ └── migration.sql │ │ │ ├── 20250221170553_add_subscriptions_table │ │ │ │ └── migration.sql │ │ │ ├── 20250516152300_add_more_roles │ │ │ │ └── migration.sql │ │ │ ├── 20250516152331_add_teams_support │ │ │ │ └── migration.sql │ │ │ ├── 20250530184132_add_polar_and_better_auth │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── server │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── index.ts │ │ │ │ ├── middleware.ts │ │ │ │ ├── routes │ │ │ │ ├── flags.ts │ │ │ │ └── jobs.tsx │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ └── project.ts │ │ ├── auth │ │ │ └── index.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ ├── history.ts │ │ │ │ ├── project.ts │ │ │ │ ├── usage.ts │ │ │ │ └── user.ts │ │ └── trpc │ │ │ ├── index.ts │ │ │ ├── root.ts │ │ │ └── routers │ │ │ ├── projectEnvironments.ts │ │ │ ├── projectFlags.ts │ │ │ ├── projectHistory.ts │ │ │ ├── projectKeys.ts │ │ │ ├── projectMembers.ts │ │ │ ├── projects.ts │ │ │ ├── subscription.ts │ │ │ ├── teamInvites.ts │ │ │ ├── teamMembers.ts │ │ │ └── teams.ts │ ├── store │ │ ├── index.ts │ │ ├── slices │ │ │ ├── app.ts │ │ │ └── modals.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── types │ │ ├── global.d.ts │ │ ├── index.ts │ │ └── styled-components.d.ts │ └── utils │ │ ├── helpers │ │ ├── general.ts │ │ └── testUtils.tsx │ │ ├── registry │ │ ├── StyledComponentsRegistry.tsx │ │ └── index.tsx │ │ └── trpc │ │ ├── query-client.ts │ │ ├── react.tsx │ │ └── server.ts ├── forms │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── (authenticated) │ │ │ └── a │ │ │ │ ├── _components │ │ │ │ ├── QuickLinks │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── RecentForms │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ └── Teams │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── form │ │ │ │ └── [formId] │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── settings │ │ │ │ │ ├── customization │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── FormFailedUrl.tsx │ │ │ │ │ │ │ ├── FormRedirectUrl.tsx │ │ │ │ │ │ │ ├── FormSendQueryString.tsx │ │ │ │ │ │ │ └── FormSuccessUrl.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── general │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── DeleteForm.tsx │ │ │ │ │ │ │ ├── EnableForm.tsx │ │ │ │ │ │ │ ├── FormDataRetention.tsx │ │ │ │ │ │ │ ├── FormEndpoint.tsx │ │ │ │ │ │ │ ├── FormKey.tsx │ │ │ │ │ │ │ ├── FormName.tsx │ │ │ │ │ │ │ ├── FormOwner.tsx │ │ │ │ │ │ │ ├── FormSpamProtection.tsx │ │ │ │ │ │ │ └── FormWebHookUrl.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── members │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ └── MembersTable.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── notifications │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ └── FormEmails.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── security │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── FormHoneyPot.tsx │ │ │ │ │ │ │ ├── FormIpRules.tsx │ │ │ │ │ │ │ └── FormWebsites.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── styles.ts │ │ │ │ │ └── utils.tsx │ │ │ │ │ ├── setup │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── EndpointCard │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── styles.ts │ │ │ │ │ │ ├── Form │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Links │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ └── SetupGuide │ │ │ │ │ │ │ ├── DefaultStep.tsx │ │ │ │ │ │ │ ├── JavascriptStep.tsx │ │ │ │ │ │ │ ├── ReactStep.tsx │ │ │ │ │ │ │ ├── RestStep.tsx │ │ │ │ │ │ │ ├── Step.tsx │ │ │ │ │ │ │ ├── VueStep.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.ts │ │ │ │ │ └── submissions │ │ │ │ │ ├── _components │ │ │ │ │ ├── FormSubmission │ │ │ │ │ │ ├── Body.tsx │ │ │ │ │ │ ├── Header │ │ │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── Labels.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── FormSubmissions │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── Toolbar │ │ │ │ │ │ ├── PopupMenu.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── styles.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── invite │ │ │ │ └── [token] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── styles.ts │ │ │ │ └── user │ │ │ │ └── tab │ │ │ │ ├── billing │ │ │ │ ├── _components │ │ │ │ │ ├── CurrentPlan │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── Meters │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── general │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── styles.ts │ │ ├── (unauthenticated) │ │ │ ├── auth │ │ │ │ └── sign-in │ │ │ │ │ └── page.tsx │ │ │ └── status │ │ │ │ ├── error │ │ │ │ └── page.tsx │ │ │ │ └── success │ │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ ├── trpc │ │ │ │ └── [trpc] │ │ │ │ │ └── route.ts │ │ │ └── v1 │ │ │ │ └── [[...route]] │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── not-found.tsx │ ├── components │ │ └── Navigation │ │ │ ├── index.tsx │ │ │ └── utils.ts │ ├── i18n │ │ └── request.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── messages │ │ └── en.json │ ├── middleware.ts │ ├── modals │ │ ├── Confirm │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Form │ │ │ ├── AddMember │ │ │ │ └── index.tsx │ │ │ └── Create │ │ │ │ └── index.tsx │ │ ├── Team │ │ │ ├── Create │ │ │ │ └── index.tsx │ │ │ └── Manage │ │ │ │ ├── Members │ │ │ │ ├── InviteForm.tsx │ │ │ │ ├── MemberCard.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ │ └── index.tsx │ │ └── index.tsx │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20231204150432_init │ │ │ │ └── migration.sql │ │ │ ├── 20240305210106_form │ │ │ │ └── migration.sql │ │ │ ├── 20240321142217_normalize_tables │ │ │ │ └── migration.sql │ │ │ ├── 20240324185640_add_security_settings_to_form │ │ │ │ └── migration.sql │ │ │ ├── 20240327173927_fix_submissions_relation_with_form │ │ │ │ └── migration.sql │ │ │ ├── 20240401154424_update_defaults │ │ │ │ └── migration.sql │ │ │ ├── 20240415184820_add_defaults_to_submissions_viewed_spam │ │ │ │ └── migration.sql │ │ │ ├── 20240417175416_add_honeypot_to_form │ │ │ │ └── migration.sql │ │ │ ├── 20240421165710_add_websites_to_security │ │ │ │ └── migration.sql │ │ │ ├── 20240513172128_add_subscription_usage │ │ │ │ └── migration.sql │ │ │ ├── 20250516151955_add_more_roles │ │ │ │ └── migration.sql │ │ │ ├── 20250516152058_add_teams_support │ │ │ │ └── migration.sql │ │ │ ├── 20250530184226_add_polar_and_better_auth │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── server │ │ ├── api │ │ │ └── v1 │ │ │ │ ├── index.ts │ │ │ │ ├── routes │ │ │ │ ├── jobs.tsx │ │ │ │ └── submissions.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ └── form.ts │ │ ├── auth │ │ │ └── index.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ ├── form.ts │ │ │ │ ├── subscription.ts │ │ │ │ └── user.ts │ │ └── trpc │ │ │ ├── index.ts │ │ │ ├── root.ts │ │ │ └── routers │ │ │ ├── formMembers.ts │ │ │ ├── formSubmissions.ts │ │ │ ├── forms.ts │ │ │ ├── subscription.ts │ │ │ ├── teamInvites.ts │ │ │ ├── teamMembers.ts │ │ │ └── teams.ts │ ├── store │ │ ├── index.ts │ │ ├── slices │ │ │ ├── app.ts │ │ │ └── modals.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── types │ │ ├── global.d.ts │ │ └── styled-components.d.ts │ └── utils │ │ ├── helpers │ │ └── general.ts │ │ ├── registry │ │ ├── StyledComponentsRegistry.tsx │ │ └── index.tsx │ │ └── trpc │ │ ├── query-client.ts │ │ ├── react.tsx │ │ └── server.ts └── landing-page │ ├── .env.example │ ├── .eslintignore │ ├── .gitignore │ ├── README.md │ ├── app │ ├── coming-soon │ │ └── page.tsx │ ├── contact │ │ └── page.tsx │ ├── enterprise │ │ └── page.tsx │ ├── layout.tsx │ ├── legal │ │ ├── cookies │ │ │ └── page.tsx │ │ ├── privacy │ │ │ └── page.tsx │ │ └── terms │ │ │ └── page.tsx │ ├── page.tsx │ ├── product │ │ ├── feature-flags │ │ │ └── page.tsx │ │ └── forms │ │ │ └── page.tsx │ ├── robots.txt │ └── sitemap.xml │ ├── components │ ├── AccessLabelsAnimation │ │ ├── index.tsx │ │ └── styles.ts │ ├── ActivityCardsAnimation │ │ ├── index.tsx │ │ └── styles.ts │ ├── AutoSlidingCards │ │ ├── index.tsx │ │ └── styles.ts │ ├── Banner │ │ ├── index.tsx │ │ └── styles.tsx │ ├── BentoCards │ │ ├── Card │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── Card │ │ └── index.tsx │ ├── Cards │ │ ├── index.tsx │ │ └── styles.ts │ ├── CarouselButtons │ │ ├── index.tsx │ │ └── styles.ts │ ├── Code │ │ ├── __tests__ │ │ │ └── __snapshots__ │ │ │ │ └── index.spec.tsx.snap │ │ ├── data.js │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── styles.ts │ ├── CodeAnimation │ │ ├── index.tsx │ │ └── styles.ts │ ├── EnvironmentToggleAnimation │ │ ├── index.tsx │ │ └── styles.ts │ ├── FlagsCardSliderAnimation │ │ ├── index.tsx │ │ └── styles.ts │ ├── Footer │ │ ├── index.tsx │ │ └── styles.ts │ ├── Form │ │ ├── index.tsx │ │ └── styles.tsx │ ├── GlobalNavigation │ │ ├── Dropdown │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── MobileMenu │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── Hero │ │ ├── index.tsx │ │ └── styles.ts │ ├── Image │ │ ├── index.tsx │ │ └── styles.ts │ ├── Logo │ │ └── index.tsx │ ├── MiniCards │ │ ├── Card │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── OrderedCards │ │ ├── Card │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── Pricing │ │ ├── index.tsx │ │ └── styles.tsx │ ├── PricingUsage │ │ ├── index.tsx │ │ └── styles.tsx │ ├── ProductNavigation │ │ ├── Dropdown │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── Questions │ │ ├── index.tsx │ │ └── styles.ts │ ├── SectionCards │ │ ├── index.tsx │ │ └── styles.ts │ ├── SectionHeader │ │ ├── index.tsx │ │ └── styles.ts │ ├── SlideCard │ │ ├── index.tsx │ │ └── styles.ts │ ├── Slider │ │ ├── index.tsx │ │ └── styles.ts │ ├── StarsPattern │ │ ├── index.tsx │ │ └── styles.tsx │ ├── VerticalCards │ │ ├── Card │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── index.tsx │ │ └── styles.ts │ ├── WaitingList │ │ ├── index.tsx │ │ └── styles.ts │ ├── index.ts │ └── styles.ts │ ├── i18n │ └── request.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── messages │ └── en.json │ ├── next-env.d.ts │ ├── next.config.ts │ ├── package.json │ ├── public │ └── images │ │ ├── og-image.jpeg │ │ └── product │ │ ├── flags │ │ ├── activity.png │ │ ├── activity_dark.png │ │ ├── flags_cards_popups.png │ │ ├── flags_cards_popups_dark.png │ │ ├── multiple_projects.png │ │ ├── multiple_projects_dark.png │ │ ├── remote_config.png │ │ └── remote_config_dark.png │ │ └── forms │ │ ├── all_forms.png │ │ ├── all_forms_dark.png │ │ ├── customize.png │ │ ├── customize_dark.png │ │ ├── security.png │ │ ├── security_dark.png │ │ ├── setup.png │ │ ├── setup_dark.png │ │ ├── submissions.png │ │ └── submissions_dark.png │ ├── store │ ├── index.ts │ ├── slices │ │ └── app.ts │ └── types.ts │ ├── styles │ ├── applGlobalStyles.ts │ ├── index.ts │ └── legalGlobalStyles.ts │ ├── tsconfig.json │ ├── types │ ├── global.d.ts │ └── styled-components.d.ts │ └── utils │ ├── helpers │ └── testUtils.tsx │ └── registry │ ├── StyledComponentsRegistry.tsx │ ├── ThemeProvider.tsx │ └── index.tsx ├── docker-compose.yml ├── package.json ├── packages ├── config │ ├── eslint-preset.js │ ├── jest.base.config.js │ ├── jest.next.config.js │ ├── jest.setup.js │ └── package.json ├── design-system │ ├── animations │ │ └── springs.ts │ ├── components │ │ ├── Accordion │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Avatar │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Banner │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Box │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Calendar │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── CalendarInput │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Card │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Checkbox │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── CircularProgress │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── CopyCard │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Empty │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Flex │ │ │ └── index.tsx │ │ ├── Grid │ │ │ └── index.tsx │ │ ├── HorizontalRule │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Icon │ │ │ ├── CustomIcon.tsx │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── icons │ │ │ │ ├── GithubIcon.tsx │ │ │ │ ├── GoogleColorsIcon.tsx │ │ │ │ ├── GoogleIcon.tsx │ │ │ │ └── index.ts │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── IconBox │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── IconButton │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Input │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── InputGroup │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Label │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Loader │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ └── index.tsx │ │ ├── Logo │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ └── index.tsx │ │ ├── Modal │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Pagination │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Pill │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Popup │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── PopupMenu │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── RangeSelector │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Search │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Segment │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Select │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ └── index.tsx │ │ ├── Skeleton │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Spinner │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Splash │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Switch │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Table │ │ │ ├── CopyClipboard.tsx │ │ │ ├── TextVisibility.tsx │ │ │ ├── __mocks__ │ │ │ │ ├── mockData.ts │ │ │ │ └── mockMembersTable.ts │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Tabs │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Text │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Textarea │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── Tooltip │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.spec.tsx.snap │ │ │ └── index.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── global │ │ ├── Portal │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.tsx.snap │ │ │ │ └── index.spec.tsx │ │ │ └── index.tsx │ │ └── testUtils.tsx │ ├── index.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── styles │ │ └── index.ts │ ├── theme │ │ ├── GlobalStyle.ts │ │ ├── common.ts │ │ ├── darkTheme.ts │ │ └── lightTheme.ts │ ├── tsconfig.json │ ├── types │ │ └── styled-components.d.ts │ └── utils │ │ └── testUtils.tsx ├── emails │ ├── .eslintrc.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── send.ts │ │ └── templates │ │ │ ├── addProjectMember.tsx │ │ │ ├── deletedAccount.tsx │ │ │ ├── index.ts │ │ │ ├── invite.tsx │ │ │ ├── newSubmission.tsx │ │ │ ├── styles.ts │ │ │ └── welcome.tsx │ └── tsconfig.json ├── hooks │ ├── .eslintrc.js │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── useDarkModeToggle.ts │ │ ├── useFloatingPopup.ts │ │ └── useIsTop.ts │ └── tsconfig.json ├── sdks │ └── flags-js │ │ ├── .eslintrc.js │ │ ├── LICENSE │ │ ├── README.md │ │ ├── examples │ │ ├── nextjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ ├── file.svg │ │ │ │ ├── globe.svg │ │ │ │ ├── next.svg │ │ │ │ ├── vercel.svg │ │ │ │ └── window.svg │ │ │ ├── src │ │ │ │ ├── app │ │ │ │ │ ├── api │ │ │ │ │ │ └── count │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── favicon.ico │ │ │ │ │ ├── globals.css │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.module.css │ │ │ │ │ └── page.tsx │ │ │ │ └── libs │ │ │ │ │ └── feature-flags │ │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useFlag.ts │ │ │ │ │ └── useFlags.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── yarn.lock │ │ └── vanilla-ts │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── public │ │ │ └── vite.svg │ │ │ ├── src │ │ │ ├── counter.ts │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ ├── typescript.svg │ │ │ └── vite-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── yarn.lock │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── yarn.lock ├── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json ├── ui │ ├── components │ │ ├── AppsDropdown │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── AvatarDropdown │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Banners │ │ │ └── index.tsx │ │ ├── CodeLanguageCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── HistoryCard │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── Navigation │ │ │ ├── Components │ │ │ │ └── ButtonLink.tsx │ │ │ ├── Desktop │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── Mobile │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── NotFound │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Plans │ │ │ ├── ActivePlan │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── PlanCard │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── UpgradePlanHeader │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── UsagePlan.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── PopupActions │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ProfileAvatarCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ProjectCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── ProjectsMenu │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SettingCard │ │ │ ├── index.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── SignIn │ │ │ ├── ProviderCard │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── icons.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── StatusPage │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SwitchSettingCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── TooltipIcon │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── UsageCard │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── UsageSection │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── index.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── tsconfig.json │ └── types │ │ └── styled-components.d.ts ├── utils │ ├── .eslintrc.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── config │ │ │ ├── defaults.ts │ │ │ ├── flags.ts │ │ │ ├── index.ts │ │ │ └── plans │ │ │ │ ├── flags.ts │ │ │ │ ├── forms.ts │ │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── auth.ts │ │ │ ├── browser.ts │ │ │ ├── formatters.ts │ │ │ ├── generators.ts │ │ │ ├── getters.ts │ │ │ ├── requestError.ts │ │ │ ├── table.ts │ │ │ ├── url.ts │ │ │ └── validators.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── cors │ │ │ │ └── index.ts │ │ │ └── headers │ │ │ │ └── index.ts │ │ └── types │ │ │ └── index.ts │ └── tsconfig.json └── vendors │ ├── index.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── src │ ├── auth │ │ ├── client.ts │ │ ├── index.ts │ │ └── server.ts │ ├── cf │ │ └── ai.ts │ ├── polar │ │ └── index.ts │ ├── qstash │ │ ├── client.ts │ │ ├── events │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── jobs │ │ │ ├── check-spam │ │ │ │ └── index.ts │ │ │ ├── check-subscription │ │ │ │ └── index.ts │ │ │ └── send-data-to-external-webhook │ │ │ │ └── index.ts │ │ └── types.ts │ └── redis │ │ └── index.ts │ └── tsconfig.json ├── render.yaml ├── turbo.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .git 4 | Dockerfile 5 | .env 6 | .dockerignore 7 | .github 8 | .vscode 9 | tsconfig.tsbuildinfo 10 | docker-compose.yml 11 | README.md 12 | .idea 13 | npm-debug.log 14 | **/.next 15 | **/dist 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Check out the global configurations for the eslint rules 2 | module.exports = require("./packages/config/eslint-preset.js"); 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | jobs: 8 | build: 9 | name: Lint, Test and Build 10 | timeout-minutes: 15 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 2 22 | 23 | - name: Setup Node.js environment 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: 22.6.0 27 | cache: "yarn" 28 | 29 | - name: Install dependencies 30 | run: yarn 31 | 32 | - name: Build packages 33 | run: yarn build:packages 34 | 35 | - name: Lint 36 | run: yarn lint 37 | 38 | # - name: Test 39 | # run: yarn test 40 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | tsconfig.tsbuildinfo 20 | .fleet 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # turbo 35 | .turbo 36 | 37 | # Jetbrains 38 | .idea 39 | 40 | #dists 41 | dist 42 | .vscode/settings.json 43 | .vercel 44 | .tool-versions 45 | 46 | .react-email 47 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Contributing to Basestack 4 | 5 | First and foremost, thank you for dedicating your time to enhance the platform. We warmly welcome all contributions. We appreciate your support in making our platform even better! 6 | 7 | For comprehensive documentation on how to contribute to the project, please refer to: [How to Contribute](https://docs.basestack.co/feature-flags/contribute) 8 | -------------------------------------------------------------------------------- /apps/docs/.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 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # Jetbrains 36 | .idea 37 | 38 | #dists 39 | dist 40 | .vscode/settings.json 41 | .vercel 42 | .tool-versions 43 | 44 | .env 45 | 46 | # generated content 47 | .contentlayer 48 | .content-collections 49 | .source 50 | -------------------------------------------------------------------------------- /apps/docs/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { HomeLayout } from "fumadocs-ui/layouts/home"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | 5 | export default function Layout({ children }: { children: ReactNode }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function HomePage() { 4 | return redirect("/content/self-hosting"); 5 | } 6 | -------------------------------------------------------------------------------- /apps/docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from "@/lib/source"; 2 | import { createFromSource } from "fumadocs-core/search/server"; 3 | 4 | // it should be cached forever 5 | export const revalidate = false; 6 | 7 | export const { staticGET: GET } = createFromSource(source); 8 | -------------------------------------------------------------------------------- /apps/docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "fumadocs-ui/css/neutral.css"; 3 | @import "fumadocs-ui/css/preset.css"; 4 | -------------------------------------------------------------------------------- /apps/docs/content/docs/feature-flags/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Basestack Feature Flags", 3 | "description": "Deploy, test, and roll out safely", 4 | "icon": "Flag", 5 | "pages": [ 6 | "---Introduction---", 7 | "index", 8 | "---SDKs---", 9 | "sdks/javascript", 10 | "sdks/react", 11 | "sdks/rest-api" 12 | ], 13 | "root": true 14 | } 15 | -------------------------------------------------------------------------------- /apps/docs/content/docs/feature-flags/sdks/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "SDKs", 3 | "icon": "MyIcon", 4 | "pages": ["javascript", "react", "rest-api"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/forms/features/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Features", 3 | "icon": "MyIcon", 4 | "pages": ["spam-protection"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/forms/guides/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Guides", 3 | "icon": "MyIcon", 4 | "pages": ["html", "javascript", "react", "vue", "rest"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/forms/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Basestack Forms", 3 | "description": "Collect form submissions easily", 4 | "icon": "File", 5 | "pages": [ 6 | "---Introduction---", 7 | "index", 8 | "getting-started", 9 | "---Features---", 10 | "features/spam-protection.mdx", 11 | "---Guides---", 12 | "guides/html", 13 | "guides/javascript", 14 | "guides/react", 15 | "guides/rest", 16 | "guides/vue" 17 | ], 18 | "root": true 19 | } 20 | -------------------------------------------------------------------------------- /apps/docs/content/docs/help.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Help & Support 3 | tags: Basestack, Platform, Help, Support, Discussion, Feedback, Bug Report, Feature Request, GitHub, Repository, Discussions 4 | --- 5 | 6 | If you need assistance with anything, whether it's a new feature, a bug, a guide, or if you'd like to contribute feedback, don't hesitate to [open a discussion](https://github.com/basestack-co/basestack/discussions/?ref=basestack.co). 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": ["platform", "feature-flags", "forms", "self-hosting"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/docs/content/docs/self-hosting/authentication/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Authentication", 3 | "icon": "MyIcon", 4 | "pages": ["github-config", "google-config"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/self-hosting/database/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Database", 3 | "icon": "MyIcon", 4 | "pages": ["neon", "supabase"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/self-hosting/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Basestack Self-Hosting", 3 | "description": "Run on your own infrastructure", 4 | "icon": "Server", 5 | "pages": [ 6 | "---Introduction---", 7 | "index", 8 | "updates", 9 | "contributing", 10 | "license", 11 | "faqs", 12 | "---Authentication---", 13 | "authentication/github-config", 14 | "authentication/google-config", 15 | "---Database---", 16 | "database/neon", 17 | "database/supabase", 18 | "---Deploying---", 19 | "providers/deploy-vercel", 20 | "providers/deploy-netlify", 21 | "providers/deploy-render" 22 | ], 23 | "root": true 24 | } 25 | -------------------------------------------------------------------------------- /apps/docs/content/docs/self-hosting/providers/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Providers", 3 | "icon": "MyIcon", 4 | "pages": ["deploy-vercel", "deploy-netlify", "deploy-render"], 5 | "defaultOpen": false 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from "@/.source"; 2 | import { loader } from "fumadocs-core/source"; 3 | import { icons } from "lucide-react"; 4 | import { createElement } from "react"; 5 | 6 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info 7 | export const source = loader({ 8 | // it assigns a URL to your pages 9 | baseUrl: "/content", 10 | source: docs.toFumadocsSource(), 11 | icon(icon) { 12 | if (!icon) { 13 | return; 14 | } 15 | if (icon in icons) return createElement(icons[icon as keyof typeof icons]); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /apps/docs/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import defaultMdxComponents from "fumadocs-ui/mdx"; 2 | import * as TabsComponents from "fumadocs-ui/components/tabs"; 3 | import type { MDXComponents } from "mdx/types"; 4 | import { ImageZoom } from "fumadocs-ui/components/image-zoom"; 5 | 6 | // use this function to get MDX components, you will need it for rendering MDX 7 | export function getMDXComponents(components?: MDXComponents): MDXComponents { 8 | return { 9 | ...defaultMdxComponents, 10 | ...TabsComponents, 11 | img: (props) => , 12 | ...components, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from "fumadocs-mdx/next"; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | output: "export", 8 | reactStrictMode: true, 9 | images: { 10 | unoptimized: true, 11 | }, 12 | /* redirects: async () => { 13 | return [ 14 | { 15 | source: "/", 16 | destination: "/docs/forms", 17 | permanent: false, 18 | }, 19 | { 20 | source: "/docs", 21 | destination: "/docs/forms", 22 | permanent: false, 23 | }, 24 | ]; 25 | }, */ 26 | }; 27 | 28 | export default withMDX(config); 29 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --port 3001", 8 | "start": "next start", 9 | "deploy": "yarn build", 10 | "postinstall": "fumadocs-mdx", 11 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next" 12 | }, 13 | "dependencies": { 14 | "fumadocs-core": "15.5.4", 15 | "fumadocs-mdx": "11.6.9", 16 | "fumadocs-ui": "15.5.4", 17 | "lucide-react": "^0.523.0", 18 | "next": "15.3.4", 19 | "react": "^19.1.0", 20 | "react-dom": "^19.1.0" 21 | }, 22 | "devDependencies": { 23 | "@basestack/config": "*", 24 | "@basestack/tsconfig": "*", 25 | "@tailwindcss/postcss": "^4.1.10", 26 | "@types/mdx": "^2.0.13", 27 | "@types/node": "24.0.3", 28 | "@types/react": "^19.1.8", 29 | "@types/react-dom": "^19.1.6", 30 | "postcss": "^8.5.5", 31 | "tailwindcss": "^4.1.10", 32 | "typescript": "^5.8.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/docs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | defineDocs, 4 | frontmatterSchema, 5 | metaSchema, 6 | } from "fumadocs-mdx/config"; 7 | 8 | // You can customise Zod schemas for frontmatter and `meta.json` here 9 | // see https://fumadocs.vercel.app/docs/mdx/collections#define-docs 10 | export const docs = defineDocs({ 11 | docs: { 12 | schema: frontmatterSchema, 13 | }, 14 | meta: { 15 | schema: metaSchema, 16 | }, 17 | }); 18 | 19 | export default defineConfig({ 20 | mdxOptions: { 21 | // MDX options 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/.source": ["./.source/index.ts"], 20 | "@/*": ["./*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "content/docs/index.mdx"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /apps/feature-flags/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /apps/feature-flags/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | tsconfig.tsbuildinfo 5 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/_components/GetStarted/TextLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTheme } from "styled-components"; 3 | import { Text } from "@basestack/design-system"; 4 | import { StyledLink } from "./styles"; 5 | 6 | interface TextLinkProps { 7 | text: string; 8 | hasMarginBottom?: boolean; 9 | link: { 10 | text: string; 11 | href: string; 12 | target?: string; 13 | }; 14 | } 15 | 16 | const TextLink = ({ text, link, hasMarginBottom = true }: TextLinkProps) => { 17 | const theme = useTheme(); 18 | 19 | return ( 20 | 21 | {text}{" "} 22 | 30 | {link.text} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default TextLink; 37 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/_components/Teams/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, SpaceProps } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Section = styled.section` 6 | display: flex; 7 | flex-direction: column; 8 | ${space}; 9 | `; 10 | 11 | export const Header = styled.div` 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | margin-bottom: ${({ theme }) => theme.spacing.s5}; 16 | min-height: ${rem("36px")}; 17 | `; 18 | 19 | export const TeamsList = styled.ul` 20 | display: grid; 21 | grid-template-columns: repeat(4, 1fr); 22 | grid-gap: ${({ theme }) => theme.spacing.s3}; 23 | 24 | @media screen and ${({ theme }) => theme.device.max.md} { 25 | grid-template-columns: repeat(2, 1fr); 26 | } 27 | 28 | @media screen and ${({ theme }) => theme.device.max.sm} { 29 | grid-template-columns: 1fr; 30 | } 31 | `; 32 | 33 | export const ListItem = styled.li` 34 | display: flex; 35 | flex-direction: column; 36 | `; 37 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/_components/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.main` 4 | padding: ${({ theme }) => theme.spacing.s6} ${({ theme }) => theme.spacing.s5}; 5 | max-width: 1100px; 6 | margin: 0 auto; 7 | width: 100%; 8 | `; 9 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/flags/_components/FlagsList/FlagCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Labels = styled.div` 5 | display: flex; 6 | flex-wrap: wrap; 7 | margin-bottom: ${({ theme }) => theme.spacing.s3}; 8 | gap: ${({ theme }) => theme.spacing.s1}; 9 | `; 10 | 11 | export const LoadingContainer = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | margin-bottom: ${({ theme }) => theme.spacing.s3}; 15 | `; 16 | 17 | export const DropdownWrapper = styled.div` 18 | position: absolute; 19 | right: ${rem("14px")}; 20 | top: ${rem("14px")}; 21 | `; 22 | 23 | export const Footer = styled.div` 24 | display: flex; 25 | align-items: center; 26 | margin-top: auto; 27 | `; 28 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/flags/_components/FlagsList/FlagCard/types.ts: -------------------------------------------------------------------------------- 1 | import { PositionProps, SpaceProps } from "styled-system"; 2 | import { PopupItemsProps } from "@basestack/design-system"; 3 | 4 | export interface FlagCardProps extends SpaceProps, PositionProps { 5 | /** 6 | * Card title 7 | */ 8 | title: string; 9 | /** 10 | * Card title 11 | */ 12 | description: string; 13 | /** 14 | * Card title 15 | */ 16 | date: string; 17 | /** 18 | * Popup items array 19 | */ 20 | popupItems?: Array; 21 | /** 22 | * Shows icon with tooltip alerting the flag is expired 23 | */ 24 | isExpired?: boolean; 25 | /** 26 | * Shows icon with tooltip alerting the flag has payload 27 | */ 28 | hasPayload?: boolean; 29 | /** 30 | * Card slug 31 | */ 32 | slug: string; 33 | /** 34 | * Card project id 35 | */ 36 | projectId: string; 37 | } 38 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/flags/_components/FlagsList/FlagRow/types.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProps, PositionProps } from "styled-system"; 2 | import { PopupItemsProps } from "@basestack/design-system"; 3 | 4 | export interface FlagRowProps extends SpaceProps, PositionProps { 5 | /** 6 | * Card title 7 | */ 8 | title: string; 9 | /** 10 | * Card title 11 | */ 12 | description: string; 13 | /** 14 | * Card title 15 | */ 16 | date: string; 17 | /** 18 | * Popup items array 19 | */ 20 | popupItems?: Array; 21 | /** 22 | * Card slug 23 | */ 24 | slug: string; 25 | /** 26 | * Card project id 27 | */ 28 | projectId: string; 29 | /** 30 | * Show icon indicating flag has payload 31 | */ 32 | hasPayload?: boolean; 33 | /** 34 | * Show icon indicating flag has expired 35 | */ 36 | isExpired?: boolean; 37 | } 38 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/flags/_components/Toolbar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, SpaceProps } from "styled-system"; 3 | 4 | export const Container = styled.header` 5 | ${space}; 6 | display: grid; 7 | grid-template-columns: repeat(4, 1fr); 8 | grid-gap: ${({ theme }) => theme.spacing.s5}; 9 | 10 | @media screen and ${({ theme }) => theme.device.max.lg} { 11 | grid-template-columns: 1fr 1fr; 12 | } 13 | 14 | @media screen and ${({ theme }) => theme.device.max.sm} { 15 | grid-template-columns: 1fr auto; 16 | } 17 | `; 18 | 19 | export const RightContent = styled.div` 20 | display: flex; 21 | align-items: center; 22 | justify-content: flex-end; 23 | 24 | @media screen and ${({ theme }) => theme.device.min.lg} { 25 | grid-column: 2 / 5; 26 | } 27 | `; 28 | 29 | export const SegmentContainer = styled.div` 30 | display: flex; 31 | align-items: center; 32 | margin-left: ${({ theme }) => theme.spacing.s5}; 33 | `; 34 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { Fragment, useEffect } from "react"; 4 | // Router 5 | import { useParams, useRouter } from "next/navigation"; 6 | // Toast 7 | import { toast } from "sonner"; 8 | // Server 9 | import { api } from "utils/trpc/react"; 10 | 11 | const ProjectLayout = ({ children }: { children: React.ReactNode }) => { 12 | const router = useRouter(); 13 | const { projectId } = useParams<{ projectId: string }>(); 14 | 15 | const { data, isLoading, isError, error } = api.projects.byId.useQuery( 16 | { projectId }, 17 | { 18 | enabled: !!projectId, 19 | retry: 1, 20 | }, 21 | ); 22 | 23 | useEffect(() => { 24 | if (isError) { 25 | toast.error(error?.message); 26 | 27 | setTimeout(() => { 28 | router.replace("/a"); 29 | }, 0); 30 | } 31 | }, [router, isError, error?.message]); 32 | 33 | if (!data || isLoading || isError) { 34 | return null; 35 | } 36 | 37 | return {children}; 38 | }; 39 | 40 | export default ProjectLayout; 41 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/settings/general/_components/Endpoints.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // Components 3 | import { CopyCard } from "@basestack/design-system"; 4 | // UI 5 | import { SettingCard } from "@basestack/ui"; 6 | // Utils 7 | import { getBrowserUrl } from "@basestack/utils"; 8 | // Locales 9 | import { useTranslations } from "next-intl"; 10 | 11 | const EndpointsCard = () => { 12 | const t = useTranslations("setting"); 13 | 14 | return ( 15 | 20 | 29 | 30 | ); 31 | }; 32 | 33 | export default EndpointsCard; 34 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/settings/general/_components/ProjectKey.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // Locales 3 | import { useTranslations } from "next-intl"; 4 | // Components 5 | import { CopyCard } from "@basestack/design-system"; 6 | // UI 7 | import { SettingCard } from "@basestack/ui"; 8 | 9 | interface ProjectKeyProps { 10 | projectKey: string; 11 | } 12 | 13 | const ProjectKey = ({ projectKey }: ProjectKeyProps) => { 14 | const t = useTranslations("setting"); 15 | 16 | return ( 17 | 22 | 31 | 32 | ); 33 | }; 34 | 35 | export default ProjectKey; 36 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/project/[projectId]/settings/members/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Router 5 | import { useParams } from "next/navigation"; 6 | // Modules 7 | import MembersTableCard from "./_components/MembersTable"; 8 | // Server 9 | import { api } from "utils/trpc/react"; 10 | // Styles 11 | import { CardList, CardListItem, SettingCardContainer } from "../styles"; 12 | 13 | const MembersPage = () => { 14 | const { projectId } = useParams<{ projectId: string }>(); 15 | const { data: project } = api.projects.byId.useQuery( 16 | { projectId }, 17 | { 18 | enabled: !!projectId, 19 | }, 20 | ); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default MembersPage; 34 | -------------------------------------------------------------------------------- /apps/feature-flags/app/(authenticated)/a/user/tab/general/_components/ThemeCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ContentContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const PopupWrapper = styled.div` 9 | height: 0; 10 | `; 11 | 12 | export const TextContainer = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | margin-bottom: ${({ theme }) => theme.spacing.s5}; 16 | `; 17 | 18 | export const MenuContainer = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | `; 22 | -------------------------------------------------------------------------------- /apps/feature-flags/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | // Auth 2 | import { auth } from "server/auth"; 3 | // Vendors 4 | import { toNextJsHandler } from "better-auth/next-js"; 5 | 6 | export const { POST, GET } = toNextJsHandler(auth); 7 | -------------------------------------------------------------------------------- /apps/feature-flags/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | // Server 2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 3 | import { appRouter } from "server/trpc/root"; 4 | import { createTRPCContext } from "server/trpc"; 5 | // Types 6 | import { type NextRequest } from "next/server"; 7 | 8 | const createContext = async (req: NextRequest) => { 9 | return createTRPCContext({ 10 | headers: req.headers, 11 | }); 12 | }; 13 | 14 | const handler = (req: NextRequest) => 15 | fetchRequestHandler({ 16 | endpoint: "/api/trpc", 17 | req, 18 | router: appRouter, 19 | createContext: () => createContext(req), 20 | onError: 21 | process.env.NODE_ENV === "development" 22 | ? ({ path, error }) => { 23 | console.error( 24 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`, 25 | ); 26 | } 27 | : undefined, 28 | }); 29 | 30 | export { handler as GET, handler as POST }; 31 | -------------------------------------------------------------------------------- /apps/feature-flags/app/api/v1/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import { handle } from "hono/vercel"; 2 | // Server 3 | import app from "server/api/v1"; 4 | 5 | export const OPTIONS = handle(app); 6 | export const GET = handle(app); 7 | export const POST = handle(app); 8 | export const PUT = handle(app); 9 | export const PATCH = handle(app); 10 | export const DELETE = handle(app); 11 | -------------------------------------------------------------------------------- /apps/feature-flags/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Router 5 | import { useRouter } from "next/navigation"; 6 | // UI 7 | import { NotFound } from "@basestack/ui"; 8 | // Locales 9 | import { useTranslations } from "next-intl"; 10 | 11 | const PageNotFound = () => { 12 | const t = useTranslations("common"); 13 | const router = useRouter(); 14 | 15 | return ( 16 | router.push("/")} 21 | /> 22 | ); 23 | }; 24 | 25 | export default PageNotFound; 26 | -------------------------------------------------------------------------------- /apps/feature-flags/components/Activity/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const ToolBar = styled.div` 9 | display: flex; 10 | margin-bottom: ${({ theme }) => theme.spacing.s6}; 11 | gap: ${({ theme }) => theme.spacing.s5}; 12 | 13 | @media screen and ${({ theme }) => theme.device.max.sm} { 14 | flex-direction: column; 15 | gap: ${({ theme }) => theme.spacing.s2}; 16 | } 17 | `; 18 | 19 | export const InputContainer = styled.div` 20 | display: flex; 21 | flex-direction: column; 22 | width: 100%; 23 | `; 24 | 25 | export const List = styled.ul` 26 | display: flex; 27 | flex-direction: column; 28 | `; 29 | 30 | export const ListItem = styled.li` 31 | display: flex; 32 | flex-direction: column; 33 | `; 34 | -------------------------------------------------------------------------------- /apps/feature-flags/components/TooltipIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SpaceProps } from "styled-system"; 3 | import { useTheme } from "styled-components"; 4 | import { 5 | Icon, 6 | Tooltip, 7 | TooltipContent, 8 | TooltipTrigger, 9 | } from "@basestack/design-system"; 10 | import { TooltipContainer } from "./styles"; 11 | 12 | interface TooltipIconProps extends SpaceProps { 13 | icon: string; 14 | text: string; 15 | } 16 | 17 | const TooltipIcon = ({ icon, text, ...props }: TooltipIconProps) => { 18 | const theme = useTheme(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 29 | 30 | {text} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default TooltipIcon; 37 | -------------------------------------------------------------------------------- /apps/feature-flags/components/TooltipIcon/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | 4 | export const TooltipContainer = styled.div` 5 | ${space}; 6 | `; 7 | -------------------------------------------------------------------------------- /apps/feature-flags/i18n/request.ts: -------------------------------------------------------------------------------- 1 | // Locales 2 | import { getRequestConfig } from "next-intl/server"; 3 | 4 | export default getRequestConfig(async () => { 5 | // Provide a static locale, fetch a user setting, 6 | // read from `cookies()`, `headers()`, etc. 7 | const locale = "en"; 8 | 9 | return { 10 | locale, 11 | messages: (await import(`../messages/${locale}.json`)).default, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /apps/feature-flags/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.next.config"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /apps/feature-flags/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | require("jest-fetch-mock").enableMocks(); 3 | 4 | module.exports = common; 5 | -------------------------------------------------------------------------------- /apps/feature-flags/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | // Auth 3 | import { createAuthClient } from "better-auth/client"; 4 | 5 | const client = createAuthClient(); 6 | 7 | export async function middleware(request: NextRequest) { 8 | const { data: session } = await client.getSession({ 9 | fetchOptions: { 10 | headers: { 11 | cookie: request.headers.get("cookie") || "", 12 | }, 13 | }, 14 | }); 15 | 16 | if (!session) { 17 | return NextResponse.redirect(new URL("/auth/sign-in", request.url)); 18 | } 19 | 20 | return NextResponse.next(); 21 | } 22 | 23 | export const config = { 24 | matcher: ["/a/:path*", "/api/trpc/:path*"], 25 | }; 26 | -------------------------------------------------------------------------------- /apps/feature-flags/modals/Confirm/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | import { space, SpaceProps } from "styled-system"; 4 | 5 | export const Container = styled.main` 6 | padding: ${({ theme }) => theme.spacing.s5}; 7 | `; 8 | 9 | export const Content = styled.p` 10 | ${space}; 11 | word-break: break-all; 12 | font-size: ${rem("14px")}; 13 | line-height: ${rem("22px")}; 14 | font-weight: 400; 15 | color: ${({ theme }) => 16 | theme.colors[theme.isDarkMode ? "gray400" : "gray500"]}; 17 | 18 | b { 19 | color: ${({ theme }) => 20 | theme.colors[theme.isDarkMode ? "gray300" : "black"]}; 21 | font-weight: 500; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /apps/feature-flags/modals/Environment/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const FormSchema = z.object({ 4 | name: z 5 | .string() 6 | .max(30, "modal.environment.input.name.error.max") 7 | .min(1, "modal.environment.input.name.error.min"), 8 | description: z 9 | .string() 10 | .max(250, "modal.environment.input.description.error.max") 11 | .min(1, "modal.environment.input.description.error.min"), 12 | }); 13 | 14 | export type FormInputs = z.TypeOf; 15 | -------------------------------------------------------------------------------- /apps/feature-flags/modals/Flag/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Environments = styled.div` 5 | display: flex; 6 | align-items: center; 7 | flex-wrap: wrap; 8 | `; 9 | 10 | export const EditorContainer = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | height: ${rem("150px")}; 14 | overflow: hidden; 15 | 16 | ${({ theme }) => 17 | theme.isDarkMode && 18 | css` 19 | .monaco-editor { 20 | --vscode-editorGutter-background: ${theme.colors.gray700}; 21 | --vscode-editor-background: ${theme.colors.gray700}; 22 | } 23 | 24 | .monaco-editor .view-overlays .current-line-exact { 25 | border-color: ${theme.colors.gray600}; 26 | } 27 | `}; 28 | `; 29 | -------------------------------------------------------------------------------- /apps/feature-flags/modals/Flag/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const EnvironmentSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | enabled: z.boolean(), 7 | payload: z.any().optional(), 8 | expiredAt: z.date().nullish().optional(), 9 | flagId: z.string().nullish().optional(), 10 | }); 11 | 12 | export const FlagFormSchema = z.object({ 13 | name: z 14 | .string() 15 | .max(150, "modal.flag.tab.core.input.name.error.max") 16 | .min(1, "modal.flag.tab.core.input.name.error.min"), 17 | description: z 18 | .string() 19 | .max(150, "modal.flag.tab.core.input.description.error.max") 20 | .optional(), 21 | environments: z.array(EnvironmentSchema), 22 | }); 23 | 24 | export type FlagFormInputs = z.TypeOf; 25 | export type EnvironmentInput = z.TypeOf; 26 | -------------------------------------------------------------------------------- /apps/feature-flags/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220316170855_add_password_field_to_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "password" TEXT; 3 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220404210013_add_project_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Project" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "slug" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | 9 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "ProjectsOnUsers" ( 14 | "projectId" TEXT NOT NULL, 15 | "userId" TEXT NOT NULL, 16 | 17 | CONSTRAINT "ProjectsOnUsers_pkey" PRIMARY KEY ("projectId","userId") 18 | ); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "Project_slug_key" ON "Project"("slug"); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "ProjectsOnUsers" ADD CONSTRAINT "ProjectsOnUsers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "ProjectsOnUsers" ADD CONSTRAINT "ProjectsOnUsers_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220418185341_update_project_fields/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `name` on the `Project` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(30)`. 5 | - You are about to alter the column `slug` on the `Project` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(30)`. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "ProjectsOnUsers" DROP CONSTRAINT "ProjectsOnUsers_projectId_fkey"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Project" ALTER COLUMN "name" SET DATA TYPE VARCHAR(30), 13 | ALTER COLUMN "slug" SET DATA TYPE VARCHAR(30); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "ProjectsOnUsers" ADD CONSTRAINT "ProjectsOnUsers_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220420162603_environments/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ALTER COLUMN "slug" SET DATA TYPE VARCHAR(150); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Environment" ( 6 | "id" TEXT NOT NULL, 7 | "name" VARCHAR(30) NOT NULL, 8 | "slug" VARCHAR(150) NOT NULL, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" TIMESTAMP(3) NOT NULL, 11 | "projectId" TEXT NOT NULL, 12 | 13 | CONSTRAINT "Environment_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "Environment_slug_key" ON "Environment"("slug"); 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "Environment" ADD CONSTRAINT "Environment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220428161711_flags/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Environment" ADD COLUMN "description" VARCHAR(250); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Flag" ( 6 | "id" TEXT NOT NULL, 7 | "slug" VARCHAR(150) NOT NULL, 8 | "description" VARCHAR(250), 9 | "enabled" BOOLEAN NOT NULL DEFAULT false, 10 | "payload" JSON DEFAULT '{}', 11 | "expiredAt" TIMESTAMP(3), 12 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | "updatedAt" TIMESTAMP(3) NOT NULL, 14 | "environmentId" TEXT NOT NULL, 15 | 16 | CONSTRAINT "Flag_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "Flag" ADD CONSTRAINT "Flag_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220502201512_history/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "History" ( 3 | "id" TEXT NOT NULL, 4 | "projectId" TEXT NOT NULL, 5 | "action" TEXT NOT NULL, 6 | "payload" JSON DEFAULT '{}', 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "History_pkey" PRIMARY KEY ("id","projectId") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "History_projectId_idx" ON "History"("projectId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "History" ADD CONSTRAINT "History_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20220605132228_add_key_to_projects_and_envs/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[key]` on the table `Environment` will be added. If there are existing duplicate values, this will fail. 5 | - A unique constraint covering the columns `[key]` on the table `Project` will be added. If there are existing duplicate values, this will fail. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Environment" ADD COLUMN "key" TEXT; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Project" ADD COLUMN "key" TEXT; 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "Environment_key_key" ON "Environment"("key"); 16 | 17 | -- CreateIndex 18 | CREATE INDEX "Environment_key_idx" ON "Environment"("key"); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "Project_key_key" ON "Project"("key"); 22 | 23 | -- CreateIndex 24 | CREATE INDEX "Project_key_idx" ON "Project"("key"); 25 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20230129180958_add_default_env_and_user_roles/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Environment" ADD COLUMN "isDefault" BOOLEAN DEFAULT false; 6 | 7 | -- AlterTable 8 | ALTER TABLE "ProjectsOnUsers" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'ADMIN'; 9 | 10 | -- AlterTable 11 | ALTER TABLE "User" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'ADMIN'; 12 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20230418100546_add_dates_to_project_on_users/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `updatedAt` to the `ProjectsOnUsers` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ProjectsOnUsers" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; 10 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20230812173446_update_models_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Environment_key_idx"; 3 | 4 | -- DropIndex 5 | DROP INDEX "History_projectId_idx"; 6 | 7 | -- DropIndex 8 | DROP INDEX "Project_key_idx"; 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Environment_key_slug_idx" ON "Environment"("key", "slug"); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "Flag_slug_environmentId_idx" ON "Flag"("slug", "environmentId"); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "Project_key_slug_idx" ON "Project"("key", "slug"); 18 | 19 | -- CreateIndex 20 | CREATE INDEX "User_email_name_idx" ON "User"("email", "name"); 21 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/20250516152300_add_more_roles/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | -- This migration adds more than one value to an enum. 3 | -- With PostgreSQL versions 11 and earlier, this is not possible 4 | -- in a single migration. This can be worked around by creating 5 | -- multiple migrations, each migration adding only one value to 6 | -- the enum. 7 | 8 | 9 | ALTER TYPE "Role" ADD VALUE 'DEVELOPER'; 10 | ALTER TYPE "Role" ADD VALUE 'VIEWER'; 11 | ALTER TYPE "Role" ADD VALUE 'TESTER'; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Subscription" ADD COLUMN "teams" INTEGER NOT NULL DEFAULT 0; 15 | -------------------------------------------------------------------------------- /apps/feature-flags/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /apps/feature-flags/server/api/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { cors } from "hono/cors"; 3 | // Utils 4 | import { logger } from "hono/logger"; 5 | // Types 6 | import { Env } from "./types"; 7 | // Routes 8 | import flags from "./routes/flags"; 9 | import jobs from "./routes/jobs"; 10 | 11 | const app = new Hono().basePath("/api/v1"); 12 | 13 | app.use( 14 | "/flags/*", 15 | cors({ 16 | origin: "*", 17 | allowMethods: ["GET"], 18 | allowHeaders: [ 19 | "Content-Type", 20 | "referer", 21 | "x-project-key", 22 | "x-environment-key", 23 | ], 24 | maxAge: 86400, 25 | credentials: false, 26 | }), 27 | ); 28 | 29 | app.use(logger()); 30 | 31 | export const routes = app.route("/flags", flags).route("/jobs", jobs); 32 | 33 | export default app; 34 | -------------------------------------------------------------------------------- /apps/feature-flags/server/api/v1/types.ts: -------------------------------------------------------------------------------- 1 | export type Env = {}; 2 | -------------------------------------------------------------------------------- /apps/feature-flags/server/db/index.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | import { PrismaClient } from ".prisma/client"; 3 | 4 | const createPrismaClient = () => 5 | new PrismaClient({ 6 | log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], 7 | }); 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: ReturnType | undefined; 11 | }; 12 | 13 | export const prisma = globalForPrisma.prisma ?? createPrismaClient(); 14 | 15 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 16 | -------------------------------------------------------------------------------- /apps/feature-flags/server/trpc/routers/projectKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | protectedProcedure, 3 | createTRPCRouter, 4 | withProjectRestrictions, 5 | } from "server/trpc"; 6 | // Utils 7 | import { z } from "zod"; 8 | 9 | export const projectKeysRouter = createTRPCRouter({ 10 | list: protectedProcedure 11 | .use(withProjectRestrictions({ roles: [] })) 12 | .input( 13 | z 14 | .object({ 15 | projectId: z.string(), 16 | }) 17 | .required(), 18 | ) 19 | .query(async ({ ctx, input }) => { 20 | const keys = await ctx.prisma.project.findUnique({ 21 | where: { 22 | id: input.projectId, 23 | }, 24 | select: { 25 | key: true, 26 | environments: { 27 | select: { 28 | id: true, 29 | name: true, 30 | key: true, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | return { keys }; 37 | }), 38 | }); 39 | -------------------------------------------------------------------------------- /apps/feature-flags/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist, createJSONStorage } from "zustand/middleware"; 3 | // Types 4 | import { Store } from "./types"; 5 | // Slices 6 | import { createModalsSlice } from "./slices/modals"; 7 | import { createAppSlice } from "./slices/app"; 8 | 9 | export const useStore = create()( 10 | persist( 11 | (...a) => ({ 12 | ...createAppSlice(...a), 13 | ...createModalsSlice(...a), 14 | }), 15 | { 16 | version: 1, 17 | name: "feature-flags-app", 18 | storage: createJSONStorage(() => localStorage), 19 | // @ts-ignore 20 | partialize: (state) => ({ 21 | isDarkMode: state.isDarkMode, 22 | selectedView: state.selectedView, 23 | numberOfFlagsPerPage: state.numberOfFlagsPerPage, 24 | closeModalsOnClickOutside: state.closeModalsOnClickOutside, 25 | closeNoActiveSubscriptionBanner: state.closeNoActiveSubscriptionBanner, 26 | }), 27 | }, 28 | ), 29 | ); 30 | -------------------------------------------------------------------------------- /apps/feature-flags/store/slices/app.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | // Types 3 | import { AppSliceState, Store } from "../types"; 4 | 5 | export const createAppSlice: StateCreator = ( 6 | set, 7 | ) => ({ 8 | numberOfFlagsPerPage: 20, 9 | closeModalsOnClickOutside: true, 10 | isDarkMode: false, 11 | selectedView: "cards", 12 | closeNoActiveSubscriptionBanner: false, 13 | setDarkMode: (payload) => 14 | set(() => ({ 15 | isDarkMode: payload, 16 | })), 17 | setSelectedView: (payload) => 18 | set(() => ({ 19 | selectedView: payload.view, 20 | })), 21 | setCloseModalsOnClickOutside: () => 22 | set((state) => ({ 23 | closeModalsOnClickOutside: !state.closeModalsOnClickOutside, 24 | })), 25 | setNumberOfFlagsPerPage: (payload) => 26 | set(() => ({ 27 | numberOfFlagsPerPage: payload, 28 | })), 29 | setCloseNoActiveSubscriptionBanner: (payload) => 30 | set(() => ({ 31 | closeNoActiveSubscriptionBanner: payload, 32 | })), 33 | }); 34 | -------------------------------------------------------------------------------- /apps/feature-flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "strict": true, 6 | "plugins": [ 7 | { 8 | "name": "next" 9 | } 10 | ] 11 | }, 12 | "include": [ 13 | "**/*.ts", 14 | "**/*.tsx", 15 | "next-env.d.ts", 16 | ".next/types/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/feature-flags/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import en from "../messages/en.json"; 2 | 3 | declare module "next-intl" { 4 | interface AppConfig { 5 | Messages: typeof en; 6 | Formats: typeof formats; 7 | } 8 | } 9 | 10 | declare global { 11 | interface Document { 12 | startViewTransition?: (callback: () => void) => { 13 | ready: Promise; 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/feature-flags/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from "@basestack/design-system/theme/lightTheme"; 2 | 3 | export type ThemeInterface = typeof theme; 4 | 5 | declare module "styled-components" { 6 | interface DefaultTheme extends ThemeInterface {} 7 | } 8 | -------------------------------------------------------------------------------- /apps/feature-flags/utils/helpers/general.ts: -------------------------------------------------------------------------------- 1 | export const AppMode = process.env.NEXT_PUBLIC_APP_MODE ?? "production"; 2 | -------------------------------------------------------------------------------- /apps/feature-flags/utils/registry/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Registries 5 | import StyledComponentsRegistry from "./StyledComponentsRegistry"; 6 | // Dates 7 | import dayjs from "dayjs"; 8 | import relativeTime from "dayjs/plugin/relativeTime"; 9 | // Components 10 | import { Toaster } from "sonner"; 11 | // TRPC 12 | import { TRPCReactProvider } from "utils/trpc/react"; 13 | 14 | // Fonts 15 | import "material-symbols/rounded.css"; 16 | 17 | dayjs.extend(relativeTime); 18 | 19 | const Registry = ({ children }: { children: React.ReactNode }) => { 20 | return ( 21 | 22 | {children} 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Registry; 29 | -------------------------------------------------------------------------------- /apps/feature-flags/utils/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { 3 | defaultShouldDehydrateQuery, 4 | QueryClient, 5 | } from "@tanstack/react-query"; 6 | import SuperJSON from "superjson"; 7 | 8 | export const createQueryClient = () => 9 | new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | // With SSR, we usually want to set some default staleTime 13 | // above 0 to avoid refetching immediately on the client 14 | staleTime: 30 * 1000, 15 | }, 16 | dehydrate: { 17 | serializeData: SuperJSON.serialize, 18 | shouldDehydrateQuery: (query) => 19 | defaultShouldDehydrateQuery(query) || 20 | query.state.status === "pending", 21 | }, 22 | hydrate: { 23 | deserializeData: SuperJSON.deserialize, 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /apps/feature-flags/utils/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc"; 4 | // NextJS 5 | import { headers } from "next/headers"; 6 | import { cache } from "react"; 7 | // TRPC 8 | import { createCaller, type AppRouter } from "server/trpc/root"; 9 | import { createTRPCContext } from "server/trpc"; 10 | import { createQueryClient } from "./query-client"; 11 | 12 | const createContext = cache(async () => { 13 | const heads = new Headers(await headers()); 14 | heads.set("x-trpc-source", "rsc"); 15 | 16 | return createTRPCContext({ 17 | headers: heads, 18 | }); 19 | }); 20 | 21 | const getQueryClient = cache(createQueryClient); 22 | const caller = createCaller(createContext); 23 | 24 | export const { trpc: api, HydrateClient } = createHydrationHelpers( 25 | caller, 26 | getQueryClient, 27 | ); 28 | -------------------------------------------------------------------------------- /apps/forms/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /apps/forms/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | tsconfig.tsbuildinfo 5 | 6 | # Sentry Config File 7 | .sentryclirc 8 | -------------------------------------------------------------------------------- /apps/forms/README.md: -------------------------------------------------------------------------------- 1 | # Forms - Basestack 2 | 3 | What is Forms - Basestack? 4 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/_components/Teams/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, SpaceProps } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Section = styled.section` 6 | display: flex; 7 | flex-direction: column; 8 | ${space}; 9 | `; 10 | 11 | export const Header = styled.div` 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | margin-bottom: ${({ theme }) => theme.spacing.s5}; 16 | min-height: ${rem("36px")}; 17 | `; 18 | 19 | export const TeamsList = styled.ul` 20 | display: grid; 21 | grid-template-columns: repeat(4, 1fr); 22 | grid-gap: ${({ theme }) => theme.spacing.s3}; 23 | 24 | @media screen and ${({ theme }) => theme.device.max.md} { 25 | grid-template-columns: repeat(2, 1fr); 26 | } 27 | 28 | @media screen and ${({ theme }) => theme.device.max.sm} { 29 | grid-template-columns: 1fr; 30 | } 31 | `; 32 | 33 | export const ListItem = styled.li` 34 | display: flex; 35 | flex-direction: column; 36 | `; 37 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { Fragment, useEffect } from "react"; 4 | // Router 5 | import { useParams, useRouter } from "next/navigation"; 6 | // Toast 7 | import { toast } from "sonner"; 8 | // Server 9 | import { api } from "utils/trpc/react"; 10 | 11 | const FormLayout = ({ children }: { children: React.ReactNode }) => { 12 | const router = useRouter(); 13 | const { formId } = useParams<{ formId: string }>(); 14 | 15 | const { data, isLoading, isError, error } = api.forms.byId.useQuery( 16 | { formId }, 17 | { 18 | enabled: !!formId, 19 | }, 20 | ); 21 | 22 | useEffect(() => { 23 | if (isError) { 24 | toast.error(error?.message); 25 | 26 | setTimeout(() => { 27 | router.replace("/a"); 28 | }, 0); 29 | } 30 | }, [router, isError, error?.message]); 31 | 32 | if (!data || isLoading || isError) { 33 | return null; 34 | } 35 | 36 | return {children}; 37 | }; 38 | 39 | export default FormLayout; 40 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/settings/general/_components/FormKey.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // UI 3 | import { SettingCard } from "@basestack/ui"; 4 | // Components 5 | import { CopyCard } from "@basestack/design-system"; 6 | // Locales 7 | import { useTranslations } from "next-intl"; 8 | 9 | export interface Props { 10 | formId: string; 11 | } 12 | 13 | const FormKeyCard = ({ formId }: Props) => { 14 | const t = useTranslations("setting"); 15 | 16 | return ( 17 | 22 | 31 | 32 | ); 33 | }; 34 | 35 | export default FormKeyCard; 36 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/settings/members/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Router 5 | import { useParams } from "next/navigation"; 6 | // Modules 7 | import MembersTableCard from "./_components/MembersTable"; 8 | // Server 9 | import { api } from "utils/trpc/react"; 10 | // Styles 11 | import { CardList, CardListItem, SettingCardContainer } from "../styles"; 12 | 13 | const MembersPage = () => { 14 | const { formId } = useParams<{ formId: string }>(); 15 | const { data: form } = api.forms.byId.useQuery( 16 | { formId }, 17 | { 18 | enabled: !!formId, 19 | }, 20 | ); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default MembersPage; 34 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/setup/_components/EndpointCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | 9 | @media screen and ${({ theme }) => theme.device.max.lg} { 10 | flex-direction: column; 11 | align-items: flex-start; 12 | justify-content: flex-start; 13 | } 14 | `; 15 | 16 | export const TextContainer = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | margin-right: ${({ theme }) => theme.spacing.s5}; 20 | `; 21 | 22 | export const InputContainer = styled.div` 23 | display: flex; 24 | align-items: center; 25 | position: relative; 26 | max-width: 400px; 27 | width: 100%; 28 | 29 | input { 30 | padding-right: 54px; 31 | } 32 | 33 | @media screen and ${({ theme }) => theme.device.max.lg} { 34 | margin-top: ${({ theme }) => theme.spacing.s3}; 35 | } 36 | `; 37 | 38 | export const TooltipContainer = styled.div` 39 | position: absolute; 40 | right: ${rem("6px")}; 41 | `; 42 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/setup/_components/Links/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const List = styled.ul` 4 | display: grid; 5 | grid-template-columns: 1fr 1fr; 6 | gap: ${({ theme }) => theme.spacing.s2}; 7 | 8 | @media screen and ${({ theme }) => theme.device.max.sm} { 9 | grid-template-columns: 1fr; 10 | } 11 | `; 12 | 13 | export const ListItem = styled.li` 14 | display: flex; 15 | `; 16 | 17 | export const Link = styled.a` 18 | position: relative; 19 | display: flex; 20 | justify-content: center; 21 | text-decoration: none; 22 | 23 | &:after { 24 | transition: width 0.2s ease-in-out; 25 | content: ""; 26 | position: absolute; 27 | bottom: 0; 28 | height: 1px; 29 | background-color: ${({ theme }) => theme.text.color}; 30 | width: 0; 31 | } 32 | 33 | p { 34 | transition: color 0.2s ease-in-out; 35 | } 36 | 37 | &:hover { 38 | p { 39 | color: ${({ theme }) => theme.text.color}; 40 | } 41 | 42 | &:after { 43 | width: 100%; 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/setup/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { 3 | space, 4 | compose, 5 | layout, 6 | flexbox, 7 | SpaceProps, 8 | FlexboxProps, 9 | LayoutProps, 10 | } from "styled-system"; 11 | 12 | export const Container = styled.main` 13 | display: flex; 14 | flex-direction: column; 15 | flex-grow: 1; 16 | padding: ${({ theme }) => theme.spacing.s6} ${({ theme }) => theme.spacing.s5}; 17 | max-width: 1440px; 18 | margin: 0 auto; 19 | width: 100%; 20 | `; 21 | 22 | type BoxProps = LayoutProps & SpaceProps & FlexboxProps; 23 | 24 | export const Box = styled.div` 25 | ${compose(layout, space, flexbox)}; 26 | `; 27 | 28 | export const Row = styled.div` 29 | display: flex; 30 | gap: ${({ theme }) => theme.spacing.s5}; 31 | 32 | @media screen and ${({ theme }) => theme.device.max.lg} { 33 | flex-direction: column; 34 | } 35 | `; 36 | 37 | export const Column = styled.div` 38 | flex: 1 0 0; 39 | `; 40 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/form/[formId]/submissions/_components/FormSubmissions/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.main` 4 | display: flex; 5 | flex-direction: column; 6 | flex-grow: 1; 7 | padding: ${({ theme }) => theme.spacing.s6} ${({ theme }) => theme.spacing.s5}; 8 | max-width: 1440px; 9 | margin: 0 auto; 10 | width: 100%; 11 | `; 12 | 13 | export const List = styled.ul` 14 | display: flex; 15 | flex-direction: column; 16 | `; 17 | 18 | export const ListItem = styled.li` 19 | display: flex; 20 | flex-direction: column; 21 | 22 | &:not(:last-child) { 23 | margin-bottom: ${({ theme }) => theme.spacing.s3}; 24 | } 25 | `; 26 | 27 | export const PaginationContainer = styled.div` 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | margin-top: auto; 32 | padding-top: ${({ theme }) => theme.spacing.s5}; 33 | `; 34 | -------------------------------------------------------------------------------- /apps/forms/app/(authenticated)/a/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.main` 4 | padding: ${({ theme }) => theme.spacing.s6} ${({ theme }) => theme.spacing.s5}; 5 | max-width: 1100px; 6 | margin: 0 auto; 7 | width: 100%; 8 | `; 9 | -------------------------------------------------------------------------------- /apps/forms/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | // Auth 2 | import { auth } from "server/auth"; 3 | // Vendors 4 | import { toNextJsHandler } from "better-auth/next-js"; 5 | 6 | export const { POST, GET } = toNextJsHandler(auth); 7 | -------------------------------------------------------------------------------- /apps/forms/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | // Server 2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 3 | import { appRouter } from "server/trpc/root"; 4 | import { createTRPCContext } from "server/trpc"; 5 | // Types 6 | import { type NextRequest } from "next/server"; 7 | 8 | const createContext = async (req: NextRequest) => { 9 | return createTRPCContext({ 10 | headers: req.headers, 11 | }); 12 | }; 13 | 14 | const handler = (req: NextRequest) => 15 | fetchRequestHandler({ 16 | endpoint: "/api/trpc", 17 | req, 18 | router: appRouter, 19 | createContext: () => createContext(req), 20 | onError: 21 | process.env.NODE_ENV === "development" 22 | ? ({ path, error }) => { 23 | console.error( 24 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`, 25 | ); 26 | } 27 | : undefined, 28 | }); 29 | 30 | export { handler as GET, handler as POST }; 31 | -------------------------------------------------------------------------------- /apps/forms/app/api/v1/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import { handle } from "hono/vercel"; 2 | // Server 3 | import app from "server/api/v1"; 4 | 5 | export const OPTIONS = handle(app); 6 | export const GET = handle(app); 7 | export const POST = handle(app); 8 | export const PUT = handle(app); 9 | export const PATCH = handle(app); 10 | export const DELETE = handle(app); 11 | -------------------------------------------------------------------------------- /apps/forms/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Router 5 | import { useRouter } from "next/navigation"; 6 | // UI 7 | import { NotFound } from "@basestack/ui"; 8 | // Locales 9 | import { useTranslations } from "next-intl"; 10 | 11 | const PageNotFound = () => { 12 | const t = useTranslations("common"); 13 | const router = useRouter(); 14 | 15 | return ( 16 | router.push("/")} 21 | /> 22 | ); 23 | }; 24 | 25 | export default PageNotFound; 26 | -------------------------------------------------------------------------------- /apps/forms/i18n/request.ts: -------------------------------------------------------------------------------- 1 | // Locales 2 | import { getRequestConfig } from "next-intl/server"; 3 | 4 | export default getRequestConfig(async () => { 5 | // Provide a static locale, fetch a user setting, 6 | // read from `cookies()`, `headers()`, etc. 7 | const locale = "en"; 8 | 9 | return { 10 | locale, 11 | messages: (await import(`../messages/${locale}.json`)).default, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /apps/forms/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.next.config"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /apps/forms/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | require("jest-fetch-mock").enableMocks(); 3 | 4 | module.exports = common; 5 | -------------------------------------------------------------------------------- /apps/forms/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | // Auth 3 | import { createAuthClient } from "better-auth/client"; 4 | 5 | const client = createAuthClient(); 6 | 7 | export async function middleware(request: NextRequest) { 8 | const { data: session } = await client.getSession({ 9 | fetchOptions: { 10 | headers: { 11 | cookie: request.headers.get("cookie") || "", 12 | }, 13 | }, 14 | }); 15 | 16 | if (!session) { 17 | return NextResponse.redirect(new URL("/auth/sign-in", request.url)); 18 | } 19 | 20 | return NextResponse.next(); 21 | } 22 | 23 | export const config = { 24 | matcher: ["/a/:path*", "/api/trpc/:path*"], 25 | }; 26 | -------------------------------------------------------------------------------- /apps/forms/modals/Confirm/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | import { space, SpaceProps } from "styled-system"; 4 | 5 | export const Container = styled.main` 6 | padding: ${({ theme }) => theme.spacing.s5}; 7 | `; 8 | 9 | export const Content = styled.p` 10 | ${space}; 11 | word-break: break-all; 12 | font-size: ${rem("14px")}; 13 | line-height: ${rem("22px")}; 14 | font-weight: 400; 15 | color: ${({ theme }) => 16 | theme.colors[theme.isDarkMode ? "gray400" : "gray500"]}; 17 | 18 | b { 19 | color: ${({ theme }) => 20 | theme.colors[theme.isDarkMode ? "gray300" : "black"]}; 21 | font-weight: 500; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /apps/forms/modals/Team/Manage/Members/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | 9 | export const InputGroupContainer = styled.div` 10 | display: flex; 11 | align-items: center; 12 | border-radius: ${rem("4px")}; 13 | gap: ${({ theme }) => theme.spacing.s2}; 14 | margin: ${({ theme }) => theme.spacing.s2} 0 15 | ${({ theme }) => theme.spacing.s6} 0; 16 | `; 17 | 18 | export const InputGroupWrapper = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | flex-grow: 1; 22 | `; 23 | 24 | export const MembersList = styled.ul` 25 | display: flex; 26 | flex-direction: column; 27 | margin-top: ${({ theme }) => theme.spacing.s2}; 28 | gap: ${({ theme }) => theme.spacing.s4}; 29 | `; 30 | 31 | export const MembersListItem = styled.li` 32 | display: flex; 33 | align-items: center; 34 | gap: ${({ theme }) => theme.spacing.s4}; 35 | `; 36 | 37 | export const MemberInfo = styled.div` 38 | display: flex; 39 | flex-direction: column; 40 | flex-grow: 1; 41 | `; 42 | -------------------------------------------------------------------------------- /apps/forms/modals/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | // Forms 3 | import CreateFormModal from "./Form/Create"; 4 | import AddFormMemberModal from "./Form/AddMember"; // Generic 5 | import ConfirmModal from "./Confirm"; 6 | // Teams 7 | import CreateTeamModal from "./Team/Create"; 8 | import ManageTeamModal from "./Team/Manage"; 9 | 10 | const Modals = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Modals; 23 | -------------------------------------------------------------------------------- /apps/forms/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240324185640_add_security_settings_to_form/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Form" ADD COLUMN "blockIpAddresses" TEXT, 3 | ADD COLUMN "emails" TEXT, 4 | ADD COLUMN "errorUrl" VARCHAR(2183), 5 | ADD COLUMN "hasDataQueryString" BOOLEAN NOT NULL DEFAULT true, 6 | ADD COLUMN "hasSpamProtection" BOOLEAN NOT NULL DEFAULT true, 7 | ADD COLUMN "rules" JSON DEFAULT '{}', 8 | ADD COLUMN "successUrl" VARCHAR(2183), 9 | ADD COLUMN "webhookUrl" VARCHAR(2183); 10 | 11 | -- AlterTable 12 | ALTER TABLE "Submission" ALTER COLUMN "isSpam" DROP NOT NULL, 13 | ALTER COLUMN "isSpam" DROP DEFAULT; 14 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240327173927_fix_submissions_relation_with_form/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Submission" ALTER COLUMN "viewed" DROP NOT NULL, 3 | ALTER COLUMN "viewed" DROP DEFAULT; 4 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240401154424_update_defaults/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Form" ALTER COLUMN "hasDataQueryString" SET DEFAULT false, 3 | ALTER COLUMN "hasSpamProtection" SET DEFAULT false; 4 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240415184820_add_defaults_to_submissions_viewed_spam/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Submission" ALTER COLUMN "isSpam" SET DEFAULT false, 3 | ALTER COLUMN "viewed" SET DEFAULT false; 4 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240417175416_add_honeypot_to_form/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Form" ADD COLUMN "honeypot" VARCHAR(30) DEFAULT '_trap', 3 | ALTER COLUMN "hasSpamProtection" SET DEFAULT true; 4 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20240421165710_add_websites_to_security/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Form" ADD COLUMN "websites" TEXT; 3 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/20250516151955_add_more_roles/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | -- This migration adds more than one value to an enum. 3 | -- With PostgreSQL versions 11 and earlier, this is not possible 4 | -- in a single migration. This can be worked around by creating 5 | -- multiple migrations, each migration adding only one value to 6 | -- the enum. 7 | 8 | 9 | ALTER TYPE "Role" ADD VALUE 'DEVELOPER'; 10 | ALTER TYPE "Role" ADD VALUE 'VIEWER'; 11 | ALTER TYPE "Role" ADD VALUE 'TESTER'; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Subscription" ADD COLUMN "teams" INTEGER NOT NULL DEFAULT 0; 15 | -------------------------------------------------------------------------------- /apps/forms/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /apps/forms/server/api/v1/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { cors } from "hono/cors"; 3 | // Utils 4 | import { logger } from "hono/logger"; 5 | // Types 6 | import { Env } from "./types"; 7 | // Routes 8 | import submissions from "./routes/submissions"; 9 | import jobs from "./routes/jobs"; 10 | 11 | const app = new Hono().basePath("/api/v1"); 12 | 13 | app.use( 14 | "/s/*", 15 | cors({ 16 | origin: "*", 17 | allowMethods: ["POST"], 18 | allowHeaders: ["Content-Type", "referer"], 19 | maxAge: 86400, 20 | credentials: false, 21 | }), 22 | ); 23 | 24 | app.use(logger()); 25 | 26 | export const routes = app.route("/s", submissions).route("/jobs", jobs); 27 | 28 | export default app; 29 | -------------------------------------------------------------------------------- /apps/forms/server/api/v1/types.ts: -------------------------------------------------------------------------------- 1 | export type Env = {}; 2 | -------------------------------------------------------------------------------- /apps/forms/server/auth/index.ts: -------------------------------------------------------------------------------- 1 | // Auth 2 | import { betterAuth } from "better-auth"; 3 | // Adapters 4 | import { prismaAdapter } from "better-auth/adapters/prisma"; 5 | // Utils 6 | import { AppMode } from "utils/helpers/general"; 7 | import { config, Product, AppEnv } from "@basestack/utils"; 8 | // DB 9 | import { prisma } from "../db"; 10 | // Vendors 11 | import { auth as authVendor } from "@basestack/vendors"; 12 | 13 | export const auth: ReturnType = authVendor.createAuthServer({ 14 | product: Product.FORMS, 15 | env: AppMode as AppEnv, 16 | database: prismaAdapter(prisma, { 17 | provider: "postgresql", 18 | }), 19 | welcomeEmail: { 20 | subject: `Welcome to Basestack Forms`, 21 | content: { 22 | title: "Welcome to Basestack Forms", 23 | description: 24 | "Welcome to Basestack Forms, the platform that elevates your website with powerful, customizable forms.", 25 | link: config.urls.getAppWithEnv(Product.FORMS, AppMode as AppEnv), 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /apps/forms/server/db/index.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | import { PrismaClient } from ".prisma/client"; 3 | 4 | const createPrismaClient = () => 5 | new PrismaClient({ 6 | log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], 7 | }); 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: ReturnType | undefined; 11 | }; 12 | 13 | export const prisma = globalForPrisma.prisma ?? createPrismaClient(); 14 | 15 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 16 | -------------------------------------------------------------------------------- /apps/forms/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand/index"; 2 | import { persist, createJSONStorage } from "zustand/middleware"; 3 | // Types 4 | import { Store } from "./types"; 5 | // Slices 6 | import { createModalsSlice } from "./slices/modals"; 7 | import { createAppSlice } from "./slices/app"; 8 | 9 | export const useStore = create()( 10 | persist( 11 | (...a) => ({ 12 | ...createAppSlice(...a), 13 | ...createModalsSlice(...a), 14 | }), 15 | { 16 | version: 1, 17 | name: "forms-app", 18 | storage: createJSONStorage(() => localStorage), 19 | // @ts-ignore 20 | partialize: (state) => ({ 21 | isDarkMode: state.isDarkMode, 22 | closeModalsOnClickOutside: state.closeModalsOnClickOutside, 23 | closeNoActiveSubscriptionBanner: state.closeNoActiveSubscriptionBanner, 24 | }), 25 | }, 26 | ), 27 | ); 28 | -------------------------------------------------------------------------------- /apps/forms/store/slices/app.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | // Types 3 | import { AppSliceState, Store } from "../types"; 4 | 5 | export const createAppSlice: StateCreator = ( 6 | set, 7 | ) => ({ 8 | closeModalsOnClickOutside: true, 9 | isDarkMode: false, 10 | selectedView: "cards", 11 | closeNoActiveSubscriptionBanner: false, 12 | setDarkMode: (payload) => 13 | set(() => ({ 14 | isDarkMode: payload, 15 | })), 16 | setCloseModalsOnClickOutside: () => 17 | set((state) => ({ 18 | closeModalsOnClickOutside: !state.closeModalsOnClickOutside, 19 | })), 20 | setCloseNoActiveSubscriptionBanner: (payload) => 21 | set(() => ({ 22 | closeNoActiveSubscriptionBanner: payload, 23 | })), 24 | }); 25 | -------------------------------------------------------------------------------- /apps/forms/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "strict": true, 6 | "plugins": [ 7 | { 8 | "name": "next" 9 | } 10 | ] 11 | }, 12 | "include": [ 13 | "next-env.d.ts", 14 | "**/*.ts", 15 | "**/*.tsx", 16 | ".next/types/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/forms/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import en from "../messages/en.json"; 2 | 3 | declare module "next-intl" { 4 | interface AppConfig { 5 | Messages: typeof en; 6 | Formats: typeof formats; 7 | } 8 | } 9 | 10 | declare global { 11 | interface Document { 12 | startViewTransition?: (callback: () => void) => { 13 | ready: Promise; 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/forms/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from "@basestack/design-system/theme/lightTheme"; 2 | 3 | export type ThemeInterface = typeof theme; 4 | 5 | declare module "styled-components" { 6 | interface DefaultTheme extends ThemeInterface {} 7 | } 8 | -------------------------------------------------------------------------------- /apps/forms/utils/helpers/general.ts: -------------------------------------------------------------------------------- 1 | export const AppMode = process.env.NEXT_PUBLIC_APP_MODE ?? "production"; 2 | -------------------------------------------------------------------------------- /apps/forms/utils/registry/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | // Registries 5 | import StyledComponentsRegistry from "./StyledComponentsRegistry"; 6 | // Dates 7 | import dayjs from "dayjs"; 8 | import relativeTime from "dayjs/plugin/relativeTime"; 9 | // Components 10 | import { Toaster } from "sonner"; 11 | // TRPC 12 | import { TRPCReactProvider } from "utils/trpc/react"; 13 | 14 | // Fonts 15 | import "material-symbols/rounded.css"; 16 | 17 | dayjs.extend(relativeTime); 18 | 19 | const Registry = ({ children }: { children: React.ReactNode }) => { 20 | return ( 21 | 22 | {children} 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Registry; 29 | -------------------------------------------------------------------------------- /apps/forms/utils/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from "@tanstack/react-query"; 5 | import SuperJSON from "superjson"; 6 | 7 | export const createQueryClient = () => 8 | new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | // With SSR, we usually want to set some default staleTime 12 | // above 0 to avoid refetching immediately on the client 13 | staleTime: 30 * 1000, 14 | }, 15 | dehydrate: { 16 | serializeData: SuperJSON.serialize, 17 | shouldDehydrateQuery: (query) => 18 | defaultShouldDehydrateQuery(query) || 19 | query.state.status === "pending", 20 | }, 21 | hydrate: { 22 | deserializeData: SuperJSON.deserialize, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /apps/forms/utils/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc"; 4 | import { headers } from "next/headers"; 5 | import { cache } from "react"; 6 | 7 | import { createCaller, type AppRouter } from "server/trpc/root"; 8 | import { createTRPCContext } from "server/trpc"; 9 | import { createQueryClient } from "./query-client"; 10 | 11 | const createContext = cache(async () => { 12 | const heads = new Headers(await headers()); 13 | heads.set("x-trpc-source", "rsc"); 14 | 15 | return createTRPCContext({ 16 | headers: heads, 17 | }); 18 | }); 19 | 20 | const getQueryClient = cache(createQueryClient); 21 | const caller = createCaller(createContext); 22 | 23 | export const { trpc: api, HydrateClient } = createHydrationHelpers( 24 | caller, 25 | getQueryClient, 26 | ); 27 | -------------------------------------------------------------------------------- /apps/landing-page/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FORM_UPDATES_ENDPOINT= -------------------------------------------------------------------------------- /apps/landing-page/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tsconfig.tsbuildinfo 3 | -------------------------------------------------------------------------------- /apps/landing-page/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | tsconfig.tsbuildinfo 5 | -------------------------------------------------------------------------------- /apps/landing-page/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/facebook/react/issues/24304#issuecomment-1094565891 2 | 3 | Yarn v1 4 | npx yarn-deduplicate --packages @types/react for now. Any package requiring v17 explicitly should be asked to either bump it (if they support React 18) or you should find an alternative library anyway if you want to use React 18. 5 | -------------------------------------------------------------------------------- /apps/landing-page/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /private/ 4 | 5 | User-agent: Googlebot 6 | Allow: /public/ 7 | Disallow: /private/ 8 | 9 | Host: https://basestack.co 10 | 11 | Sitemap: https://basestack.co/sitemap.xml 12 | -------------------------------------------------------------------------------- /apps/landing-page/components/AccessLabelsAnimation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: flex-end; 8 | justify-content: center; 9 | padding: ${({ theme }) => theme.spacing.s4}; 10 | flex-grow: 1; 11 | `; 12 | 13 | export const ContentContainer = styled.div` 14 | display: flex; 15 | flex-direction: column; 16 | align-items: flex-end; 17 | gap: ${({ theme }) => theme.spacing.s2}; 18 | `; 19 | 20 | export const Labels = styled.div` 21 | display: flex; 22 | align-items: center; 23 | flex-wrap: nowrap; 24 | min-height: ${rem("36px")}; 25 | gap: ${({ theme }) => theme.spacing.s2}; 26 | `; 27 | 28 | export const LabelContainer = styled.div` 29 | display: inline-flex; 30 | `; 31 | 32 | export const ButtonContainer = styled.div` 33 | button, 34 | button:hover { 35 | background-color: ${({ theme }) => 36 | theme.isDarkMode 37 | ? theme.colors.gray600 38 | : theme.colors.gray300} !important; 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /apps/landing-page/components/ActivityCardsAnimation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | flex-grow: 1; 9 | `; 10 | 11 | export const List = styled.ul` 12 | display: flex; 13 | flex-direction: column; 14 | padding: ${({ theme }) => theme.spacing.s2}; 15 | `; 16 | 17 | export const ListItem = styled.li` 18 | display: flex; 19 | flex-direction: column; 20 | `; 21 | -------------------------------------------------------------------------------- /apps/landing-page/components/BentoCards/Card/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const CardContainer = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | height: 100%; 8 | position: relative; 9 | cursor: default; 10 | `; 11 | 12 | const containerStyles = css` 13 | display: flex; 14 | flex-direction: column; 15 | border-radius: ${rem("8px")}; 16 | overflow: hidden; 17 | margin-top: ${({ theme }) => theme.spacing.s5}; 18 | `; 19 | 20 | export const ComponentContainer = styled.div` 21 | ${containerStyles}; 22 | min-height: 360px; 23 | background-color: ${({ theme }) => 24 | theme.isDarkMode ? theme.colors.gray900 : theme.colors.gray50}; 25 | `; 26 | 27 | export const ImageContainer = styled.div` 28 | ${containerStyles}; 29 | max-height: 360px; 30 | `; 31 | 32 | export const Image = styled.img` 33 | width: 100%; 34 | height: 100%; 35 | object-fit: cover; 36 | border-radius: ${rem("8px")}; 37 | `; 38 | -------------------------------------------------------------------------------- /apps/landing-page/components/BentoCards/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.section` 5 | display: flex; 6 | flex-direction: column; 7 | padding: ${rem("100px")} ${({ theme }) => theme.spacing.s5}; 8 | `; 9 | 10 | export const HeaderContainer = styled.section` 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | `; 15 | 16 | export const ContentContainer = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | max-width: ${rem("1400px")}; 21 | width: 100%; 22 | margin: 0 auto; 23 | `; 24 | 25 | export const Grid = styled.div` 26 | display: grid; 27 | grid-template-columns: repeat(2, minmax(0, 1fr)); 28 | gap: ${({ theme }) => theme.spacing.s5}; 29 | width: 100%; 30 | 31 | @media screen and ${({ theme }) => theme.device.max.xl} { 32 | gap: ${({ theme }) => theme.spacing.s2}; 33 | } 34 | 35 | @media screen and ${({ theme }) => theme.device.max.md} { 36 | grid-template-columns: minmax(0, 1fr); 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /apps/landing-page/components/CodeAnimation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | flex-grow: 1; 10 | `; 11 | 12 | export const Text = styled.div` 13 | font-family: ${({ theme }) => theme.typography.robotoMono}; 14 | font-size: ${rem("18px")}; 15 | font-weight: 400; 16 | white-space: pre; 17 | color: ${({ theme }) => 18 | theme.isDarkMode ? theme.colors.gray300 : theme.colors.black}; 19 | `; 20 | 21 | export const TextWrapper = styled.span``; 22 | -------------------------------------------------------------------------------- /apps/landing-page/components/EnvironmentToggleAnimation/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Switch, Text } from "@basestack/design-system"; 3 | import { Container, ContentContainer, Item } from "./styles"; 4 | 5 | type Environment = { name: string; enabled: boolean }; 6 | 7 | interface EnvironmentToggleAnimationProps { 8 | environments: Array; 9 | onChange: (env: Environment) => void; 10 | } 11 | 12 | const EnvironmentToggleAnimation = ({ 13 | environments, 14 | onChange, 15 | }: EnvironmentToggleAnimationProps) => { 16 | return ( 17 | 18 | 19 | {environments.map((environment, index) => ( 20 | 21 | {environment.name} 22 | 25 | onChange({ ...environment, enabled: !environment.enabled }) 26 | } 27 | /> 28 | 29 | ))} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default EnvironmentToggleAnimation; 36 | -------------------------------------------------------------------------------- /apps/landing-page/components/EnvironmentToggleAnimation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | flex-grow: 1; 10 | `; 11 | 12 | export const ContentContainer = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | gap: ${({ theme }) => theme.spacing.s4}; 16 | min-width: ${rem("200px")}; 17 | `; 18 | 19 | export const Item = styled.div` 20 | display: flex; 21 | align-items: center; 22 | flex-wrap: nowrap; 23 | justify-content: space-between; 24 | gap: ${({ theme }) => theme.spacing.s2}; 25 | `; 26 | -------------------------------------------------------------------------------- /apps/landing-page/components/FlagsCardSliderAnimation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | flex-grow: 1; 8 | justify-content: center; 9 | `; 10 | 11 | export const Embla = styled.div` 12 | width: 100%; 13 | `; 14 | 15 | export const EmblaViewport = styled.div` 16 | overflow: hidden; 17 | padding: ${({ theme }) => theme.spacing.s2} ${({ theme }) => theme.spacing.s5}; 18 | `; 19 | 20 | export const EmblaContainer = styled.ul` 21 | backface-visibility: hidden; 22 | display: flex; 23 | touch-action: pan-y pinch-zoom; 24 | margin-left: ${rem("-10px")}; 25 | `; 26 | 27 | export const EmblaSlide = styled.li` 28 | min-width: 0; 29 | display: flex; 30 | padding-left: ${rem("10px")}; 31 | flex: 0 0 240px; 32 | `; 33 | -------------------------------------------------------------------------------- /apps/landing-page/components/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // Components 3 | import { Container, Image } from "./styles"; 4 | import { SpaceProps } from "styled-system"; 5 | 6 | export interface ImageProps extends SpaceProps { 7 | src: string; 8 | alt: string; 9 | } 10 | 11 | const ImageComp = ({ src = "", alt = "", ...props }: ImageProps) => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default ImageComp; 20 | -------------------------------------------------------------------------------- /apps/landing-page/components/Image/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | import { space } from "styled-system"; 4 | 5 | export const Container = styled.div` 6 | ${space}; 7 | display: flex; 8 | flex-direction: column; 9 | background-color: ${({ theme }) => 10 | theme.isDarkMode ? theme.colors.gray900 : theme.colors.gray50}; 11 | box-shadow: ${({ theme }) => theme.shadow.elevation4}; 12 | border-radius: ${rem("8px")}; 13 | width: 100%; 14 | overflow: hidden; 15 | `; 16 | 17 | export const Image = styled.img` 18 | width: 100%; 19 | height: auto; 20 | `; 21 | -------------------------------------------------------------------------------- /apps/landing-page/components/MiniCards/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "./Card"; 3 | import { Container, ContentContainer, Grid, HeaderContainer } from "./styles"; 4 | import SectionHeader from "../SectionHeader"; 5 | 6 | export interface CardsProps { 7 | id?: string; 8 | title: string; 9 | caption?: string; 10 | text?: string; 11 | cards: Array<{ 12 | title: string; 13 | description?: string; 14 | icon: string; 15 | }>; 16 | } 17 | 18 | const MiniCards = ({ title, text, cards, id, caption }: CardsProps) => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | {cards?.map((card, index) => ( 26 | 32 | ))} 33 | 34 | 35 | 36 | ); 37 | 38 | export default MiniCards; 39 | -------------------------------------------------------------------------------- /apps/landing-page/components/OrderedCards/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.section` 5 | display: flex; 6 | flex-direction: column; 7 | padding: ${rem("100px")} ${({ theme }) => theme.spacing.s5}; 8 | `; 9 | 10 | export const HeaderContainer = styled.section` 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | `; 15 | 16 | export const ContentContainer = styled.div` 17 | display: flex; 18 | flex-direction: column; 19 | max-width: ${rem("1100px")}; 20 | width: 100%; 21 | margin: 0 auto; 22 | `; 23 | 24 | export const List = styled.ol` 25 | display: flex; 26 | flex-direction: column; 27 | gap: ${({ theme }) => theme.spacing.s5}; 28 | `; 29 | 30 | export const ListItem = styled.li` 31 | display: flex; 32 | flex-direction: column; 33 | `; 34 | -------------------------------------------------------------------------------- /apps/landing-page/components/ProductNavigation/Dropdown/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { position, PositionProps } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Container = styled.div` 6 | display: none; 7 | margin-left: ${({ theme }) => theme.spacing.s1}; 8 | 9 | @media screen and ${({ theme }) => theme.device.max.md} { 10 | display: flex; 11 | } 12 | `; 13 | 14 | export const Dropdown = styled.div` 15 | ${position}; 16 | background-color: ${({ theme }) => theme.popup.backgroundColor}; 17 | box-shadow: ${({ theme }) => theme.shadow.elevation6}; 18 | border-radius: 4px; 19 | width: ${rem("220px")}; 20 | z-index: ${({ theme }) => theme.zIndex.popup}; 21 | padding: ${({ theme }) => theme.spacing.s1}; 22 | `; 23 | 24 | export const List = styled.ul` 25 | display: flex; 26 | flex-direction: column; 27 | `; 28 | 29 | export const ListItem = styled.li` 30 | display: flex; 31 | flex-direction: column; 32 | `; 33 | -------------------------------------------------------------------------------- /apps/landing-page/components/Questions/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // Components 3 | import { Accordion } from "@basestack/design-system"; 4 | import { AccordionsContainer, Container, ContentContainer } from "./styles"; 5 | import SectionHeader from "../SectionHeader"; 6 | 7 | export interface QuestionsProps { 8 | id?: string; 9 | title: string; 10 | caption?: string; 11 | text: string; 12 | data: Array<{ title: string; text: string }>; 13 | } 14 | 15 | const Questions = ({ title, text, data, id, caption }: QuestionsProps) => { 16 | return ( 17 | 18 | 19 | 20 | 21 | {data?.map((item, index) => ( 22 | 23 | ))} 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Questions; 31 | -------------------------------------------------------------------------------- /apps/landing-page/components/Questions/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const Container = styled.section` 5 | display: flex; 6 | flex-direction: column; 7 | padding: ${rem("100px")} ${({ theme }) => theme.spacing.s5}; 8 | `; 9 | 10 | export const ContentContainer = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | max-width: ${rem("1400px")}; 15 | width: 100%; 16 | margin: 0 auto; 17 | `; 18 | 19 | export const AccordionsContainer = styled.div` 20 | display: flex; 21 | flex-direction: column; 22 | width: 100%; 23 | `; 24 | -------------------------------------------------------------------------------- /apps/landing-page/components/StarsPattern/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Stars = styled.div` 4 | position: absolute; 5 | inset: 0; 6 | z-index: 1; 7 | overflow: hidden; 8 | width: 100%; 9 | pointer-events: none; 10 | `; 11 | 12 | export const Star = styled.span` 13 | position: absolute; 14 | `; 15 | -------------------------------------------------------------------------------- /apps/landing-page/i18n/request.ts: -------------------------------------------------------------------------------- 1 | // Locales 2 | import { getRequestConfig } from "next-intl/server"; 3 | 4 | export default getRequestConfig(async () => { 5 | // Provide a static locale, fetch a user setting, 6 | // read from `cookies()`, `headers()`, etc. 7 | const locale = "en"; 8 | 9 | return { 10 | locale, 11 | messages: (await import(`../messages/${locale}.json`)).default, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /apps/landing-page/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.next.config"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /apps/landing-page/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | 3 | require("jest-fetch-mock").enableMocks(); 4 | 5 | module.exports = common; 6 | -------------------------------------------------------------------------------- /apps/landing-page/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/landing-page/next.config.ts: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from "next-intl/plugin"; 2 | import type { NextConfig } from "next"; 3 | 4 | const withNextIntl = createNextIntlPlugin(); 5 | 6 | const nextConfig: NextConfig = { 7 | reactStrictMode: true, 8 | output: "export", 9 | transpilePackages: ["@basestack/design-system", "@basestack/ui"], 10 | logging: { 11 | fetches: { 12 | fullUrl: true, 13 | }, 14 | }, 15 | compiler: { 16 | styledComponents: true, 17 | }, 18 | }; 19 | 20 | export default withNextIntl(nextConfig); 21 | -------------------------------------------------------------------------------- /apps/landing-page/public/images/og-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/og-image.jpeg -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/activity.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/activity_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/activity_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/flags_cards_popups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/flags_cards_popups.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/flags_cards_popups_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/flags_cards_popups_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/multiple_projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/multiple_projects.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/multiple_projects_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/multiple_projects_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/remote_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/remote_config.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/flags/remote_config_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/flags/remote_config_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/all_forms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/all_forms.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/all_forms_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/all_forms_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/customize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/customize.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/customize_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/customize_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/security.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/security_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/security_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/setup.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/setup_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/setup_dark.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/submissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/submissions.png -------------------------------------------------------------------------------- /apps/landing-page/public/images/product/forms/submissions_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/public/images/product/forms/submissions_dark.png -------------------------------------------------------------------------------- /apps/landing-page/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist, createJSONStorage } from "zustand/middleware"; 3 | // Types 4 | import { Store } from "./types"; 5 | // Slices 6 | import { createAppSlice } from "./slices/app"; 7 | 8 | export const useStore = create()( 9 | persist( 10 | (...a) => ({ 11 | ...createAppSlice(...a), 12 | }), 13 | { 14 | version: 1, 15 | name: "landing-page", 16 | storage: createJSONStorage(() => localStorage), 17 | // @ts-ignore 18 | partialize: (state) => ({ 19 | isDarkMode: state.isDarkMode, 20 | }), 21 | }, 22 | ), 23 | ); 24 | 25 | // @ts-ignore 26 | export const clearLocalStorage = () => useStore.persist.clearStorage(); 27 | -------------------------------------------------------------------------------- /apps/landing-page/store/slices/app.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | // Types 3 | import { AppSliceState, Store } from "../types"; 4 | 5 | export const createAppSlice: StateCreator = ( 6 | set, 7 | ) => ({ 8 | isDarkMode: false, 9 | setDarkMode: (payload) => 10 | set(() => ({ 11 | isDarkMode: payload, 12 | })), 13 | 14 | stargazers: 0, 15 | setStargazers: (payload) => 16 | set(() => ({ 17 | stargazers: payload, 18 | })), 19 | }); 20 | -------------------------------------------------------------------------------- /apps/landing-page/store/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * App 3 | */ 4 | export interface AppSliceActions { 5 | setDarkMode: (value: boolean) => void; 6 | setStargazers: (value: number) => void; 7 | } 8 | 9 | export interface AppSliceState extends AppSliceActions { 10 | isDarkMode: boolean; 11 | stargazers: number; 12 | } 13 | 14 | export type Store = AppSliceState; 15 | -------------------------------------------------------------------------------- /apps/landing-page/styles/applGlobalStyles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | const AppGlobalStyle = createGlobalStyle` 4 | html { 5 | scroll-behavior: smooth; 6 | display: flex; 7 | flex-direction: column; 8 | min-height: 100%; 9 | } 10 | body { 11 | display: flex; 12 | flex-direction: column; 13 | flex-grow: 1; 14 | } 15 | `; 16 | 17 | export default AppGlobalStyle; 18 | -------------------------------------------------------------------------------- /apps/landing-page/styles/index.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const DarkContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | background-color: ${({ theme }) => theme.colors.gray800}; 7 | `; 8 | -------------------------------------------------------------------------------- /apps/landing-page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "strict": true, 6 | "plugins": [ 7 | { 8 | "name": "next" 9 | } 10 | ] 11 | }, 12 | "include": [ 13 | "**/*.ts", 14 | "**/*.tsx", 15 | "next-env.d.ts", 16 | ".next/types/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/landing-page/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import en from "../messages/en.json"; 2 | 3 | declare module "next-intl" { 4 | interface AppConfig { 5 | Messages: typeof en; 6 | Formats: typeof formats; 7 | } 8 | } 9 | 10 | declare global { 11 | interface Document { 12 | startViewTransition?: (callback: () => void) => { 13 | ready: Promise; 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/landing-page/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from "@basestack/design-system/theme/lightTheme"; 2 | 3 | export type ThemeInterface = typeof theme; 4 | 5 | declare module "styled-components" { 6 | interface DefaultTheme extends ThemeInterface {} 7 | } 8 | -------------------------------------------------------------------------------- /apps/landing-page/utils/registry/ThemeProvider.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/apps/landing-page/utils/registry/ThemeProvider.tsx -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | feature-flags: 5 | container_name: basestack-feature-flags 6 | build: 7 | context: . 8 | dockerfile: ./apps/feature-flags/Dockerfile 9 | env_file: 10 | - ./apps/feature-flags/.env 11 | restart: always 12 | ports: 13 | - 3000:3000 14 | networks: 15 | - app_network 16 | 17 | # Define a network, which allows containers to communicate 18 | # with each other, by using their container name as a hostname 19 | networks: 20 | app_network: 21 | external: true 22 | -------------------------------------------------------------------------------- /packages/config/eslint-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "prettier"], 3 | settings: { 4 | next: { 5 | rootDir: ["apps/*/", "packages/*/"], 6 | }, 7 | }, 8 | rules: { 9 | "@next/next/no-html-link-for-pages": "off", 10 | "@typescript-eslint/no-empty-function": "off", 11 | "no-console": ["warn", { allow: ["error", "info"] }], 12 | "no-extra-semi": "error", 13 | "no-duplicate-imports": "error", 14 | "prefer-const": "warn", 15 | eqeqeq: "error", 16 | "no-unused-expressions": "warn", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/config/jest.base.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | testEnvironment: "jest-environment-jsdom", 4 | coverageDirectory: "coverage", 5 | roots: [""], 6 | testMatch: ["**/?(*.)+(spec).+(ts|tsx|js|jsx)"], 7 | transform: { "^.+\\.(ts|tsx|jsx)$": "ts-jest" }, 8 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 9 | modulePathIgnorePatterns: ["/dist/"], 10 | setupFilesAfterEnv: ["/jest.setup.js"], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/config/jest.next.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require("next/jest"); 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: "./", 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | // Add more setup options before each test is run 11 | // setupFilesAfterEnv: ['/jest.setup.js'], 12 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 13 | moduleDirectories: ["node_modules", "/"], 14 | testEnvironment: "jest-environment-jsdom", 15 | setupFilesAfterEnv: ["/jest.setup.js"], 16 | }; 17 | 18 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 19 | module.exports = createJestConfig(customJestConfig); 20 | -------------------------------------------------------------------------------- /packages/config/jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | require("@testing-library/jest-dom"); 7 | require("jest-styled-components"); 8 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/config", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "AGPL-3.0", 6 | "files": [ 7 | "eslint-preset.js", 8 | "jest.config.js", 9 | "jest.setup.js" 10 | ], 11 | "scripts": { 12 | "clean": "rm -rf .turbo && rm -rf node_modules" 13 | }, 14 | "dependencies": { 15 | "eslint-config-next": "^15.3.4", 16 | "eslint-config-prettier": "^10.1.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/design-system/components/Accordion/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | border-bottom: 1px solid ${({ theme }) => theme.accordion.border.color}; 7 | `; 8 | 9 | export const Header = styled.button` 10 | border: none; 11 | background-color: transparent; 12 | cursor: pointer; 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | padding: ${({ theme }) => theme.spacing.s5} 0; 17 | transition: opacity 0.2s ease-in-out; 18 | 19 | &:hover { 20 | opacity: 0.7; 21 | } 22 | `; 23 | 24 | export const ContentContainer = styled.div` 25 | display: flex; 26 | flex-direction: column; 27 | overflow: hidden; 28 | `; 29 | 30 | export const ContentWrapper = styled.div` 31 | display: flex; 32 | flex-direction: column; 33 | padding-bottom: ${({ theme }) => theme.spacing.s5}; 34 | `; 35 | -------------------------------------------------------------------------------- /packages/design-system/components/Avatar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { space } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Container = styled.div.withConfig({ 6 | shouldForwardProp: (prop) => prop !== "round", 7 | })<{ size: number; round: boolean }>` 8 | height: ${({ size }) => rem(`${size}px`)}; 9 | width: ${({ size }) => rem(`${size}px`)}; 10 | border-radius: 50%; 11 | overflow: hidden; 12 | flex-shrink: 0; 13 | ${space}; 14 | 15 | ${({ round }) => 16 | round 17 | ? css` 18 | border-radius: 50%; 19 | ` 20 | : css` 21 | border-radius: 4px; 22 | `} 23 | `; 24 | 25 | export const Image = styled.img` 26 | height: 100%; 27 | width: 100%; 28 | object-fit: cover; 29 | `; 30 | -------------------------------------------------------------------------------- /packages/design-system/components/Calendar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, forwardRef } from "react"; 2 | // Calendar 3 | import Calendar, { CalendarProps } from "react-calendar"; 4 | // Components 5 | import { Container } from "./styles"; 6 | import Icon from "../Icon"; 7 | 8 | const CalendarComp = forwardRef((props, ref) => { 9 | return ( 10 | 11 | } 14 | prev2Label={} 15 | nextLabel={} 16 | next2Label={} 17 | /> 18 | 19 | ); 20 | }); 21 | 22 | CalendarComp.displayName = "CalendarComp"; 23 | 24 | export default memo(CalendarComp); 25 | -------------------------------------------------------------------------------- /packages/design-system/components/CalendarInput/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, compose, layout } from "styled-system"; 3 | 4 | export const Container = styled.div` 5 | ${compose(space, layout)}; 6 | display: flex; 7 | flex-direction: column; 8 | `; 9 | 10 | export const CalendarReference = styled.div` 11 | height: 0; 12 | pointer-events: none; 13 | `; 14 | 15 | export const CalendarWrapper = styled.div` 16 | z-index: ${({ theme }) => theme.zIndex.calendar}; 17 | `; 18 | 19 | export const InputContainer = styled.div` 20 | display: flex; 21 | flex-direction: column; 22 | position: relative; 23 | `; 24 | -------------------------------------------------------------------------------- /packages/design-system/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo, forwardRef } from "react"; 2 | import { Container } from "./styles"; 3 | import { CardProps, Variant } from "./types"; 4 | 5 | const Card = forwardRef( 6 | ( 7 | { 8 | variant = Variant.DEFAULT, 9 | children, 10 | testId = "card", 11 | hasHoverAnimation = false, 12 | color, 13 | ...props 14 | }, 15 | ref, 16 | ) => ( 17 | 25 | {children} 26 | 27 | ), 28 | ); 29 | 30 | Card.displayName = "Card"; 31 | 32 | export { type CardProps, Variant as CardVariant }; 33 | 34 | export default memo(Card); 35 | -------------------------------------------------------------------------------- /packages/design-system/components/Card/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { 3 | LayoutProps, 4 | PositionProps, 5 | SpaceProps, 6 | ColorProps, 7 | } from "styled-system"; 8 | 9 | export enum Variant { 10 | DEFAULT = "default", 11 | DANGER = "danger", 12 | PRIMARY = "primary", 13 | WARNING = "warning", 14 | SUCCESS = "success", 15 | } 16 | 17 | export interface CardProps 18 | extends SpaceProps, 19 | PositionProps, 20 | LayoutProps, 21 | ColorProps { 22 | /** 23 | * Content 24 | */ 25 | children: ReactNode; 26 | /** 27 | * TestID 28 | */ 29 | testId?: string; 30 | /** 31 | * Optional hover animation 32 | */ 33 | hasHoverAnimation?: boolean; 34 | /** 35 | * changes styles for ex Outline color 36 | */ 37 | variant?: Variant; 38 | } 39 | -------------------------------------------------------------------------------- /packages/design-system/components/Checkbox/types.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from "react"; 2 | 3 | export type Variant = "button" | "default"; 4 | 5 | export interface CheckboxProps { 6 | checked: boolean; 7 | onChange: (event: ChangeEvent) => void; 8 | label?: string; 9 | variant?: Variant; 10 | disabled?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /packages/design-system/components/CircularProgress/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CircleWrapper = styled.div<{ size: number }>` 4 | display: inline-flex; 5 | flex-shrink: 0; 6 | align-items: center; 7 | justify-content: center; 8 | position: relative; 9 | width: ${({ size }) => size}px; 10 | height: ${({ size }) => size}px; 11 | `; 12 | 13 | export const Svg = styled.svg` 14 | transform: rotate(-90deg); 15 | width: 100%; 16 | height: 100%; 17 | `; 18 | 19 | export const CircleProgress = styled.circle` 20 | stroke-linecap: round; 21 | transition: stroke-dashoffset 0.5s ease; 22 | `; 23 | 24 | export const TextContainer = styled.div` 25 | position: absolute; 26 | `; 27 | -------------------------------------------------------------------------------- /packages/design-system/components/CopyCard/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import CopyCard from ".."; 4 | 5 | describe("CopyCard tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render CopyCard correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 18 | ); 19 | expect(asFragment()).toMatchSnapshot(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/design-system/components/Empty/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Empty from ".."; 4 | 5 | describe("Empty tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Empty correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/design-system/components/Empty/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, SpaceProps } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Container = styled.div.withConfig({ 6 | shouldForwardProp: (prop) => !["p"].includes(prop), 7 | })` 8 | display: flex; 9 | flex-direction: column; 10 | background-color: ${({ theme }) => theme.empty.backgroundColor}; 11 | border-radius: ${rem("6px")}; 12 | padding: ${({ theme }) => theme.spacing.s8} ${({ theme }) => theme.spacing.s5}; 13 | border-radius: ${rem("6px")}; 14 | justify-content: center; 15 | align-items: center; 16 | ${space}; 17 | `; 18 | -------------------------------------------------------------------------------- /packages/design-system/components/Grid/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { grid, GridProps as SSGridProps } from "styled-system"; 3 | 4 | // Define only the props that grid uses, to filter from DOM 5 | const styledSystemProps = new Set([ 6 | "gridGap", 7 | "gridColumnGap", 8 | "gridRowGap", 9 | "gridColumn", 10 | "gridRow", 11 | "gridArea", 12 | "gridAutoFlow", 13 | "gridAutoRows", 14 | "gridAutoColumns", 15 | "gridTemplateRows", 16 | "gridTemplateColumns", 17 | "gridTemplateAreas", 18 | "placeItems", 19 | "placeContent", 20 | "placeSelf", 21 | ]); 22 | 23 | const shouldForwardProp = (prop: string) => !styledSystemProps.has(prop); 24 | 25 | export interface GridProps extends SSGridProps {} 26 | 27 | const Grid = styled.div.withConfig({ 28 | shouldForwardProp, 29 | })` 30 | display: grid; 31 | ${grid}; 32 | `; 33 | 34 | export default Grid; 35 | -------------------------------------------------------------------------------- /packages/design-system/components/HorizontalRule/__tests__/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hr tests render Hr correctly 1`] = ` 4 | 5 | .c0 { 6 | display: flex; 7 | flex-direction: column; 8 | flex-shrink: 0; 9 | height: 1px; 10 | border: none; 11 | background-color: #EEEEEE; 12 | } 13 | 14 | 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /packages/design-system/components/HorizontalRule/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Hr from ".."; 4 | 5 | describe("Hr tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("render Hr correctly", () => { 9 | const { asFragment } = renderWithTheme(); 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/design-system/components/HorizontalRule/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { SpaceProps } from "styled-system"; 3 | import { Hr } from "./styles"; 4 | 5 | interface HorizontalRuleProps extends SpaceProps { 6 | isDarker?: boolean; 7 | } 8 | const HorizontalRule = ({ 9 | isDarker = false, 10 | ...props 11 | }: HorizontalRuleProps) => ( 12 | 13 | ); 14 | 15 | export default memo(HorizontalRule); 16 | -------------------------------------------------------------------------------- /packages/design-system/components/HorizontalRule/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | 4 | export const Hr = styled.hr.withConfig({ 5 | shouldForwardProp: (prop) => prop !== "isDarker", 6 | })<{ isDarker: boolean }>` 7 | ${space}; 8 | display: flex; 9 | flex-direction: column; 10 | flex-shrink: 0; 11 | height: 1px; 12 | border: none; 13 | background-color: ${({ theme, isDarker }) => 14 | isDarker 15 | ? theme.horizontalRule.darker.backgroundColor 16 | : theme.horizontalRule.backgroundColor}; 17 | `; 18 | -------------------------------------------------------------------------------- /packages/design-system/components/Icon/CustomIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTheme } from "styled-components"; 3 | import { CustomIconCompProps, CustomIconProps, CustomIconNames } from "./types"; 4 | import { GoogleIcon, GoogleColorsIcon, GithubIcon } from "./icons"; 5 | 6 | const iconMap: Record> = { 7 | github: GithubIcon, 8 | google: GoogleIcon, 9 | google_colors: GoogleColorsIcon, 10 | }; 11 | 12 | // Convert the keys of the iconMap into an array 13 | export const iconNamesArray = Object.keys(iconMap) as Array; 14 | 15 | const CustomIcon = ({ icon, size, color }: CustomIconCompProps) => { 16 | const theme = useTheme(); 17 | const IconComponent = iconMap[icon]; 18 | 19 | if (!IconComponent) { 20 | return null; 21 | } 22 | 23 | return ; 24 | }; 25 | 26 | export default CustomIcon; 27 | -------------------------------------------------------------------------------- /packages/design-system/components/Icon/icons/GoogleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CustomIconProps } from "../types"; 3 | 4 | const GoogleIcon = ({ size, color }: CustomIconProps) => ( 5 | 6 | 10 | 11 | ); 12 | 13 | export default GoogleIcon; 14 | -------------------------------------------------------------------------------- /packages/design-system/components/Icon/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GoogleIcon } from "./GoogleIcon"; 2 | export { default as GoogleColorsIcon } from "./GoogleColorsIcon"; 3 | export { default as GithubIcon } from "./GithubIcon"; 4 | -------------------------------------------------------------------------------- /packages/design-system/components/Icon/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Container = styled.div.withConfig({ 6 | shouldForwardProp: (prop) => !["size"].includes(prop), 7 | })<{ size?: number }>` 8 | height: ${({ size }) => rem(`${size}px`)}; 9 | width: ${({ size }) => rem(`${size}px`)}; 10 | flex-shrink: 0; 11 | ${space}; 12 | `; 13 | 14 | export const Icon = styled.span.withConfig({ 15 | shouldForwardProp: (prop) => !["size", "muted", "color"].includes(prop), 16 | })<{ size?: number; muted?: boolean; color?: string }>` 17 | color: ${({ theme, color, muted }) => 18 | color || (muted ? theme.icon.muted : theme.icon.color)}; 19 | font-size: ${({ size }) => rem(`${size}px`)}; 20 | `; 21 | -------------------------------------------------------------------------------- /packages/design-system/components/Icon/types.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProps } from "styled-system"; 2 | 3 | export type Size = "xxLarge" | "xLarge" | "large" | "medium" | "small"; 4 | 5 | export interface IconProps extends SpaceProps { 6 | /** 7 | * Icon name 8 | */ 9 | icon: string; 10 | /** 11 | * Icon size 12 | */ 13 | size?: Size; 14 | /** 15 | * Icon color 16 | */ 17 | color?: string; 18 | /** 19 | * Icon muted color 20 | */ 21 | muted?: boolean; 22 | } 23 | 24 | export type CustomIconNames = "github" | "google" | "google_colors"; 25 | 26 | export interface CustomIconProps { 27 | size: number; 28 | color?: string; 29 | } 30 | 31 | export interface CustomIconCompProps extends CustomIconProps { 32 | icon: CustomIconNames; 33 | } 34 | -------------------------------------------------------------------------------- /packages/design-system/components/IconBox/types.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProps } from "styled-system"; 2 | 3 | export type Size = "small" | "medium" | "large"; 4 | 5 | export type IconBoxColor = "blue" | "purple" | "gray" | "green"; 6 | 7 | export type IconBoxVariant = "outlined" | "filled"; 8 | 9 | export type Gradient = Array; 10 | 11 | export interface IconBoxProps extends SpaceProps { 12 | /** 13 | * Changes the background color if the variant is outlined 14 | * */ 15 | backgroundColor?: string; 16 | /** 17 | * Changes the icon 18 | * */ 19 | icon: string; 20 | /**** 21 | * Icon color 22 | * */ 23 | color?: IconBoxColor; 24 | /**** 25 | * Variant filled or outlined 26 | * */ 27 | variant?: IconBoxVariant; 28 | /**** 29 | * Changes the border gradient 30 | * */ 31 | gradient?: Gradient; 32 | /**** 33 | * Changes the icon color 34 | * */ 35 | iconColor?: string; 36 | /**** 37 | * Changes the size 38 | * */ 39 | size?: Size; 40 | } 41 | -------------------------------------------------------------------------------- /packages/design-system/components/IconButton/types.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SpaceProps, FlexboxProps, PositionProps } from "styled-system"; 3 | 4 | export type Size = "xLarge" | "large" | "mediumLarge" | "medium" | "small"; 5 | 6 | export type Variant = 7 | | "primary" 8 | | "primaryNeutral" 9 | | "secondary" 10 | | "secondaryDark" 11 | | "neutral"; 12 | 13 | export interface IconButtonProps 14 | extends SpaceProps, 15 | FlexboxProps, 16 | PositionProps { 17 | /** 18 | * Pass ref to element 19 | */ 20 | ref?: React.Ref; 21 | /** 22 | * Change button variant 23 | */ 24 | variant?: Variant; 25 | /** 26 | * onClick function 27 | */ 28 | onClick: (e: React.MouseEvent) => void; 29 | /** 30 | * Icon 31 | */ 32 | icon: string; 33 | /** 34 | * Optional size 35 | */ 36 | size?: Size; 37 | /** 38 | * isDisabled state 39 | */ 40 | isDisabled?: boolean; 41 | } 42 | -------------------------------------------------------------------------------- /packages/design-system/components/InputGroup/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | 4 | export const Container = styled.div` 5 | ${space}; 6 | `; 7 | 8 | export const Header = styled.div` 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | margin-bottom: ${({ theme }) => theme.spacing.s2}; 13 | `; 14 | -------------------------------------------------------------------------------- /packages/design-system/components/Label/types.ts: -------------------------------------------------------------------------------- 1 | export type LabelVariant = 2 | | "success" 3 | | "default" 4 | | "info" 5 | | "warning" 6 | | "danger" 7 | | "light"; 8 | export type LabelSize = "normal" | "medium" | "small"; 9 | -------------------------------------------------------------------------------- /packages/design-system/components/Loader/__tests__/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Loader tests should render Loader correctly 1`] = ``; 4 | -------------------------------------------------------------------------------- /packages/design-system/components/Loader/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Loader from ".."; 4 | 5 | describe("Loader tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Loader correctly", () => { 9 | const { asFragment } = renderWithTheme(); 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/design-system/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, ReactNode } from "react"; 2 | 3 | export interface LoaderProps { 4 | hasDelay?: boolean; 5 | delayTime?: number; 6 | children?: ReactNode; 7 | } 8 | 9 | const Loader = ({ 10 | children, 11 | hasDelay = true, 12 | delayTime = 1000, 13 | }: LoaderProps) => { 14 | const [show, setShow] = useState(false); 15 | 16 | useEffect(() => { 17 | if (hasDelay) { 18 | const timeout = setTimeout(() => { 19 | setShow(true); 20 | }, delayTime); 21 | 22 | return () => { 23 | clearTimeout(timeout); 24 | }; 25 | } 26 | }, [hasDelay, delayTime]); 27 | 28 | if (!show && hasDelay) return null; 29 | 30 | return <>{children}>; 31 | }; 32 | 33 | export default Loader; 34 | -------------------------------------------------------------------------------- /packages/design-system/components/Logo/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Logo from ".."; 4 | 5 | describe("Logo tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Flags Logo", () => { 9 | const { asFragment } = renderWithTheme(); 10 | 11 | expect(asFragment()).toMatchSnapshot(); 12 | }); 13 | 14 | test("should render Forms Logo", () => { 15 | const { asFragment } = renderWithTheme(); 16 | 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/design-system/components/Modal/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import type { ButtonProps } from "../Button"; 3 | export type Size = "small" | "medium" | "large" | "fullWidth"; 4 | 5 | export interface ModalProps { 6 | /** 7 | * Modal size 8 | */ 9 | size?: Size; 10 | /** 11 | * Modal title 12 | */ 13 | title: string; 14 | /** 15 | * onClose callback 16 | */ 17 | onClose: () => void; 18 | /** 19 | * Footer buttons 20 | */ 21 | buttons?: Array; 22 | /** 23 | * If modal is open 24 | */ 25 | isOpen: boolean; 26 | /** 27 | * Sets a min-height to the sheet modal 28 | */ 29 | minHeight?: number; 30 | /** 31 | * Sets the modal to full height in mobile 32 | */ 33 | expandMobile?: boolean; 34 | /** 35 | * A React Element 36 | */ 37 | children: ReactNode; 38 | /** 39 | * Callback when the animation ends 40 | */ 41 | onAnimationEnd?: () => void; 42 | /** 43 | * Enables or disables close modal on click outside main content 44 | */ 45 | closeOnClickOutside?: boolean; 46 | } 47 | -------------------------------------------------------------------------------- /packages/design-system/components/Pagination/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | import Text from "../Text"; 4 | 5 | export const Container = styled.div` 6 | ${space}; 7 | display: flex; 8 | align-items: center; 9 | `; 10 | 11 | export const StyledText = styled(Text)` 12 | ${space}; 13 | display: flex; 14 | font-weight: 500; 15 | `; 16 | 17 | export const Number = styled.div<{ highlight?: boolean }>` 18 | background-color: ${({ theme }) => theme.pagination.number.backgroundColor}; 19 | height: 32px; 20 | min-width: 32px; 21 | padding: 0 ${({ theme }) => theme.spacing.s2}; 22 | border-radius: 4px; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | `; 27 | -------------------------------------------------------------------------------- /packages/design-system/components/Pill/__tests__/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pill tests should render Pill correctly 1`] = ` 4 | 5 | .c1 { 6 | font-size: 0.875rem; 7 | line-height: 1.375rem; 8 | font-weight: 400; 9 | font-family: 'Roboto',sans-serif; 10 | word-break: break-word; 11 | color: #000000; 12 | font-family: roboto; 13 | } 14 | 15 | .c0 { 16 | border: none; 17 | cursor: pointer; 18 | display: inline-flex; 19 | align-items: center; 20 | height: 2.25rem; 21 | border-radius: 1.125rem; 22 | padding: 0 0.75rem; 23 | background-color: #EEEEEE; 24 | transition: background-color 0.1s ease-in-out; 25 | } 26 | 27 | .c0:hover:not(:active) { 28 | background-color: #E2E2E2; 29 | } 30 | 31 | 35 | 41 | pill 42 | 43 | 44 | 45 | `; 46 | -------------------------------------------------------------------------------- /packages/design-system/components/Pill/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Pill from ".."; 4 | 5 | describe("Pill tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Pill correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 11 | ); 12 | expect(asFragment()).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/design-system/components/Pill/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { SpaceProps } from "styled-system"; 3 | import Text from "../Text"; 4 | import { Container } from "./styles"; 5 | import { useTheme } from "styled-components"; 6 | 7 | export interface PillProps extends SpaceProps { 8 | /** 9 | * Changes styles to selected Pill 10 | */ 11 | isSelected: boolean; 12 | /** 13 | * onClick callback 14 | */ 15 | onClick: () => void; 16 | /** 17 | * Pill text 18 | */ 19 | text: string; 20 | } 21 | 22 | const Pill = ({ isSelected = true, onClick, text, ...props }: PillProps) => { 23 | const theme = useTheme(); 24 | 25 | return ( 26 | 32 | 37 | {text} 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default memo(Pill); 44 | -------------------------------------------------------------------------------- /packages/design-system/components/Pill/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | import { rem } from "polished"; 4 | 5 | export const Container = styled.button<{ isSelected: boolean }>` 6 | border: none; 7 | cursor: pointer; 8 | ${space}; 9 | display: inline-flex; 10 | align-items: center; 11 | height: ${rem("36px")}; 12 | border-radius: ${rem("18px")}; 13 | padding: 0 ${rem("12px")}; 14 | background-color: ${({ theme, isSelected }) => 15 | isSelected 16 | ? theme.pill.selected.backgroundColor 17 | : theme.pill.backgroundColor}; 18 | transition: background-color 0.1s ease-in-out; 19 | 20 | &:hover:not(:active) { 21 | background-color: ${({ theme, isSelected }) => 22 | isSelected 23 | ? theme.pill.hover.selected.backgroundColor 24 | : theme.pill.hover.backgroundColor}; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /packages/design-system/components/Popup/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | import { position } from "styled-system"; 4 | 5 | export const Container = styled.div<{ width?: number }>` 6 | ${position}; 7 | background-color: ${({ theme }) => theme.popup.backgroundColor}; 8 | box-shadow: ${({ theme }) => theme.shadow.elevation6}; 9 | padding: ${({ theme }) => theme.spacing.s1}; 10 | border-radius: 4px; 11 | width: ${({ width }) => (width ? rem(`${width}px`) : rem("150px"))}; 12 | z-index: ${({ theme }) => theme.zIndex.popup}; 13 | `; 14 | 15 | export const List = styled.ul` 16 | display: flex; 17 | flex-direction: column; 18 | `; 19 | 20 | export const ListItem = styled.li` 21 | display: flex; 22 | flex-direction: column; 23 | `; 24 | -------------------------------------------------------------------------------- /packages/design-system/components/PopupMenu/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: inline-flex; 5 | `; 6 | -------------------------------------------------------------------------------- /packages/design-system/components/Select/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Select from ".."; 4 | 5 | describe("Select tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Select correctly", () => { 9 | const { asFragment } = renderWithTheme(); 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/design-system/components/Skeleton/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Skeleton from ".."; 4 | 5 | describe("Skeleton tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Skeleton correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 19 | ); 20 | expect(asFragment()).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/design-system/components/Splash/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Splash from ".."; 4 | 5 | describe("Splash tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Splash correctly", () => { 9 | const { asFragment } = renderWithTheme(); 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/design-system/components/Splash/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | // Components 3 | import Logo from "../Logo"; 4 | import { Container, Content, Loader, LoaderContainer } from "./styles"; 5 | 6 | interface SplashProps { 7 | product?: "flags" | "forms"; 8 | } 9 | 10 | const Splash = ({ product = "flags" }: SplashProps) => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default memo(Splash); 22 | -------------------------------------------------------------------------------- /packages/design-system/components/Switch/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Switch from ".."; 4 | 5 | describe("Switch tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Switch with default props", () => { 9 | const { asFragment } = renderWithTheme(); 10 | expect(asFragment()).toMatchSnapshot(); 11 | }); 12 | 13 | test("should render Switch with text", () => { 14 | const { asFragment } = renderWithTheme( 15 | , 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | 20 | test("should render Switch checked by default", () => { 21 | const { asFragment } = renderWithTheme( 22 | , 23 | ); 24 | expect(asFragment()).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/design-system/components/Table/CopyClipboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | // Components 3 | import IconButton from "../IconButton"; 4 | import { Tooltip, TooltipContent, TooltipTrigger } from "../Tooltip"; 5 | import { tooltip } from "./types"; 6 | 7 | export interface CopyClipboardProps { 8 | tooltip: tooltip; 9 | } 10 | 11 | const CopyClipboard = ({ tooltip }: CopyClipboardProps) => { 12 | const [showTooltipSuccess, setShowTooltipSuccess] = useState(false); 13 | 14 | return ( 15 | 16 | setShowTooltipSuccess(false)}> 17 | { 21 | await navigator.clipboard.writeText(tooltip.textToCopy); 22 | setShowTooltipSuccess(true); 23 | }} 24 | /> 25 | 26 | 27 | {showTooltipSuccess ? tooltip.successText : tooltip.defaultText} 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default CopyClipboard; 34 | -------------------------------------------------------------------------------- /packages/design-system/components/Tabs/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Tabs from ".."; 4 | 5 | describe("Tabs tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Tabs correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 17 | ); 18 | 19 | expect(asFragment()).toMatchSnapshot(); 20 | }); 21 | 22 | test("should render Tabs with space utils", () => { 23 | const { asFragment } = renderWithTheme( 24 | , 32 | ); 33 | 34 | expect(asFragment()).toMatchSnapshot(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/design-system/components/Text/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { 3 | SpaceProps, 4 | TypographyProps, 5 | FlexboxProps, 6 | LayoutProps, 7 | } from "styled-system"; 8 | 9 | export type Size = 10 | | "xxLarge" 11 | | "xLarge" 12 | | "large" 13 | | "medium" 14 | | "small" 15 | | "xSmall"; 16 | 17 | export type FontFamily = "roboto" | "robotoFlex"; 18 | 19 | export interface TextProps 20 | extends SpaceProps, 21 | TypographyProps, 22 | FlexboxProps, 23 | LayoutProps { 24 | /** 25 | * Change text tag 26 | */ 27 | as?: string; 28 | /** 29 | * Change text size 30 | */ 31 | size?: Size; 32 | /** 33 | * Changes text to gray 34 | */ 35 | muted?: boolean; 36 | /** 37 | * Change text color 38 | */ 39 | color?: string; 40 | /** 41 | * Text 42 | */ 43 | children: ReactNode; 44 | /** 45 | * Change text fontFamily 46 | */ 47 | fontFamily?: FontFamily; 48 | /** 49 | * Truncate text to one line 50 | */ 51 | lineTruncate?: boolean; 52 | } 53 | -------------------------------------------------------------------------------- /packages/design-system/components/Textarea/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import { renderWithTheme } from "../../../utils/testUtils"; 3 | import Textarea from ".."; 4 | 5 | describe("Textarea tests", () => { 6 | afterEach(cleanup); 7 | 8 | test("should render Textarea correctly", () => { 9 | const { asFragment } = renderWithTheme( 10 | , 16 | ); 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | 20 | test("should render textarea dark", () => { 21 | const { asFragment } = renderWithTheme( 22 | , 29 | ); 30 | expect(asFragment()).toMatchSnapshot(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/design-system/components/Tooltip/__tests__/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tooltip tests should render Tooltip correctly 1`] = ` 4 | 5 | .c1 { 6 | height: 1.125rem; 7 | width: 1.125rem; 8 | flex-shrink: 0; 9 | } 10 | 11 | .c2 { 12 | color: black; 13 | font-size: 1.125rem; 14 | } 15 | 16 | .c0 { 17 | background-color: transparent; 18 | border: none; 19 | cursor: default; 20 | } 21 | 22 | 26 | 30 | 34 | info 35 | 36 | 37 | 38 | 39 | `; 40 | -------------------------------------------------------------------------------- /packages/design-system/components/Tooltip/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/react"; 2 | import Icon from "../../Icon"; 3 | import { renderWithTheme } from "../../../utils/testUtils"; 4 | import { Tooltip, TooltipContent, TooltipTrigger } from ".."; 5 | 6 | describe("Tooltip tests", () => { 7 | afterEach(cleanup); 8 | 9 | test("should render Tooltip correctly", () => { 10 | const { asFragment } = renderWithTheme( 11 | 12 | 13 | 14 | 15 | Tooltip content 16 | , 17 | ); 18 | expect(asFragment()).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/design-system/components/Tooltip/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const StyledContent = styled.div` 4 | background-color: ${({ theme }) => theme.tooltip.backgroundColor}; 5 | box-shadow: ${({ theme }) => theme.shadow.elevation3}; 6 | color: ${({ theme }) => theme.tooltip.color}; 7 | padding: 4px 8px; 8 | border-radius: 4px; 9 | box-sizing: border-box; 10 | width: max-content; 11 | max-width: calc(100vw - 10px); 12 | font-family: ${({ theme }) => theme.typography.roboto}; 13 | font-size: 14px; 14 | z-index: 1000; 15 | `; 16 | 17 | export const StyledTrigger = styled.div` 18 | background-color: transparent; 19 | border: none; 20 | cursor: default; 21 | `; 22 | -------------------------------------------------------------------------------- /packages/design-system/global/Portal/__tests__/__snapshots__/index.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Portal Component Portal renders correctly 1`] = ` 4 | 7 | Portal Content 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /packages/design-system/global/Portal/__tests__/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from "@testing-library/react"; 2 | import Portal from ".."; 3 | 4 | afterEach(cleanup); 5 | 6 | describe("Portal Component", () => { 7 | it("Portal renders correctly", () => { 8 | const { container } = render( 9 | 10 | Portal Content 11 | , 12 | ); 13 | // @ts-ignore 14 | expect(container.firstChild).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/design-system/global/Portal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState, JSX } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | interface PortalProps { 5 | children: React.ReactNode; 6 | selector: string; 7 | } 8 | 9 | const Portal = ({ children, selector }: PortalProps): JSX.Element => { 10 | const ref = useRef(null); 11 | const [mounted, setMounted] = useState(false); 12 | 13 | useEffect(() => { 14 | // @ts-ignore 15 | ref.current = document.querySelector(selector); 16 | setMounted(true); 17 | }, [selector]); 18 | 19 | // @ts-ignore 20 | return <>{mounted ? createPortal(children, ref.current) : null}>; 21 | }; 22 | 23 | export default Portal; 24 | -------------------------------------------------------------------------------- /packages/design-system/global/testUtils.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, RenderOptions } from "@testing-library/react"; 3 | // Styles 4 | import { ThemeProvider } from "styled-components"; 5 | import theme from "../theme/lightTheme"; 6 | 7 | interface Props { 8 | children: React.ReactNode; 9 | } 10 | 11 | const WithThemeProvider = ({ children }: Props) => ( 12 | {children} 13 | ); 14 | 15 | const renderWithTheme = ( 16 | ui: React.ReactElement, 17 | options?: Omit, 18 | ) => render(ui, { wrapper: WithThemeProvider, ...options }); 19 | 20 | export { renderWithTheme }; 21 | -------------------------------------------------------------------------------- /packages/design-system/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | setupFilesAfterEnv: ["/jest.setup.js"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/design-system/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /packages/design-system/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const scrollbar = css` 4 | /* width */ 5 | &::-webkit-scrollbar { 6 | width: 4px; 7 | } 8 | 9 | /* Track */ 10 | &::-webkit-scrollbar-track { 11 | background: ${({ theme }) => 12 | theme.colors[theme.isDarkMode ? "gray700" : "gray100"]}; 13 | } 14 | 15 | /* Handle */ 16 | &::-webkit-scrollbar-thumb { 17 | background: ${({ theme }) => 18 | theme.colors[theme.isDarkMode ? "gray600" : "gray300"]}; 19 | } 20 | 21 | /* Handle on hover */ 22 | &::-webkit-scrollbar-thumb:hover { 23 | background: ${({ theme }) => 24 | theme.colors[theme.isDarkMode ? "gray500" : "gray400"]}; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /packages/design-system/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest", "@testing-library/jest-dom"], 5 | "lib": ["ES2015", "dom"], 6 | "baseUrl": ".", 7 | "noEmit": true 8 | }, 9 | "include": ["."], 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/design-system/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from "../theme/lightTheme"; 2 | 3 | export type ThemeInterface = typeof theme; 4 | 5 | declare module "styled-components" { 6 | interface DefaultTheme extends ThemeInterface {} 7 | } 8 | -------------------------------------------------------------------------------- /packages/design-system/utils/testUtils.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import isPropValid from "@emotion/is-prop-valid"; 3 | import { ThemeProvider, StyleSheetManager } from "styled-components"; 4 | import defaultTheme from "@basestack/design-system/theme/lightTheme"; 5 | import { render, RenderResult } from "@testing-library/react"; 6 | 7 | const Provider = ({ 8 | children, 9 | theme = defaultTheme, 10 | }: { 11 | children: React.ReactNode; 12 | theme: any; 13 | }) => ( 14 | 15 | 16 | {children} 17 | 18 | 19 | ); 20 | 21 | export const renderWithTheme = ( 22 | ui: React.ReactElement, 23 | options?: any, 24 | ): RenderResult => render(ui, { wrapper: Provider, ...options }); 25 | -------------------------------------------------------------------------------- /packages/emails/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@basestack/config/eslint-preset"); 2 | -------------------------------------------------------------------------------- /packages/emails/src/index.ts: -------------------------------------------------------------------------------- 1 | // Templates 2 | export * from "./templates"; 3 | 4 | // Utils 5 | export * from "@react-email/render"; 6 | export * from "./send"; 7 | -------------------------------------------------------------------------------- /packages/emails/src/send.ts: -------------------------------------------------------------------------------- 1 | // Utils 2 | import nodemailer from "nodemailer"; 3 | 4 | export interface SendEmailOptions { 5 | from: string; 6 | to: string; 7 | subject: string; 8 | } 9 | export interface SendEmail { 10 | html: string; 11 | options: SendEmailOptions; 12 | } 13 | 14 | const transporter = nodemailer.createTransport({ 15 | host: process.env.EMAIL_SMTP_HOST, 16 | port: Number(process.env.EMAIL_SMTP_PORT) ?? 465, 17 | secure: !!process.env.EMAIL_SMTP_SECURE, 18 | auth: { 19 | user: process.env.EMAIL_SMTP_AUTH_USER, 20 | pass: process.env.EMAIL_SMTP_AUTH_PASSWORD, 21 | }, 22 | }); 23 | 24 | export const sendEmail = async ({ html, options }: SendEmail) => { 25 | if (!process.env.EMAIL_SMTP_HOST) { 26 | throw new Error("EMAIL_SMTP_HOST is not defined"); 27 | } 28 | 29 | // This code is needed to work with Next.js API routes 30 | return await new Promise((resolve, reject) => { 31 | transporter.sendMail({ ...options, html }, (error, info) => 32 | error ? reject(error) : resolve(info), 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/emails/src/templates/index.ts: -------------------------------------------------------------------------------- 1 | export { default as NewSubmissionEmailTemplate } from "./newSubmission"; 2 | export { default as WelcomeEmailTemplate } from "./welcome"; 3 | export { default as InviteEmailTemplate } from "./invite"; 4 | export { default as AddProjectMemberEmailTemplate } from "./addProjectMember"; 5 | export { default as DeletedAccountEmailTemplate } from "./deletedAccount"; 6 | -------------------------------------------------------------------------------- /packages/emails/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "lib": ["es5", "es6", "dom"] 6 | }, 7 | "include": ["**/*.ts", "**/*.tsx"], 8 | "exclude": ["node_modules", "dist", ".react-email"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/hooks/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@basestack/config/eslint-preset"); 2 | -------------------------------------------------------------------------------- /packages/hooks/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | setupFilesAfterEnv: ["/jest.setup.js"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/hooks/jest.setup.js: -------------------------------------------------------------------------------- 1 | require("@testing-library/jest-dom"); 2 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/hooks", 3 | "version": "0.0.0", 4 | "main": "./dist/index.js", 5 | "types": "./dist/index.d.ts", 6 | "source": "./src/index.ts", 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "license": "AGPL-3.0", 11 | "scripts": { 12 | "build": "tsup src/index.ts --format esm,cjs --dts --external react --minify", 13 | "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react", 14 | "test:blocked": "jest", 15 | "test:watch": "jest --watchAll", 16 | "test:coverage": "jest --coverage", 17 | "test:log": "jest --no-coverage >& ./logs/jest.log", 18 | "lint": "TIMING=1 eslint src --fix", 19 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "19.1.8", 23 | "@types/react-dom": "^19.1.6", 24 | "raf-stub": "^3.0.0", 25 | "react": "19.1.0", 26 | "tsup": "^8.5.0", 27 | "typescript": "^5.8.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useIsTop } from "./useIsTop"; 2 | export { default as useFloatingPopup } from "./useFloatingPopup"; 3 | export { default as useDarkModeToggle } from "./useDarkModeToggle"; 4 | -------------------------------------------------------------------------------- /packages/hooks/src/useIsTop.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | 3 | interface Props { 4 | offSet?: number; 5 | } 6 | 7 | const useIsTop = ({ offSet = 0 }: Props) => { 8 | const [isTop, setIsTop] = useState(false); 9 | const element = useRef(null); 10 | 11 | useEffect(() => { 12 | const handleScroll = () => { 13 | setIsTop( 14 | element.current 15 | ? element.current.getBoundingClientRect().top <= offSet 16 | : false, 17 | ); 18 | }; 19 | window.addEventListener("scroll", handleScroll); 20 | return () => { 21 | window.removeEventListener("scroll", handleScroll); 22 | }; 23 | }, [offSet]); 24 | 25 | return [isTop, element] as const; 26 | }; 27 | 28 | export default useIsTop; 29 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "lib": ["es5", "es6", "dom"] 6 | }, 7 | "include": ["."], 8 | "exclude": ["dist", "build", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@basestack/config/eslint-preset"); 2 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0", 14 | "next": "15.1.6" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^19", 20 | "@types/react-dom": "^19" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/app/api/count/route.ts: -------------------------------------------------------------------------------- 1 | import { ServerFlagsSDK } from "@/libs/feature-flags/server"; 2 | 3 | export async function GET() { 4 | const flagsClient = ServerFlagsSDK.getInstance(); 5 | 6 | const flag = await flagsClient.getFlag("count"); 7 | const data = await flagsClient.getAllFlags(); 8 | 9 | return Response.json({ data, flag }); 10 | } 11 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basestack-co/basestack/3a1639bbb5da37ff02facffa0a0fcb350dcf725f/packages/sdks/flags-js/examples/nextjs/src/app/favicon.ico -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./globals.css"; 3 | // Feature Flags 4 | import FeatureFlagsProvider from "@/libs/feature-flags"; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: Readonly<{ 9 | children: React.ReactNode; 10 | }>) { 11 | return ( 12 | 13 | 14 | 22 | {children} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/libs/feature-flags/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useFlag"; 2 | export * from "./useFlags"; 3 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/src/libs/feature-flags/server.ts: -------------------------------------------------------------------------------- 1 | // Note: Change to the correct dependency path 2 | import { FlagsSDK, SDKConfig } from "../../../../../dist"; 3 | 4 | export class ServerFlagsSDK { 5 | private static instance: FlagsSDK; 6 | private static config: SDKConfig = { 7 | baseURL: process.env.NEXT_PUBLIC_FEATURE_FLAGS_BASE_URL, 8 | projectKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_PROJECT_KEY!, 9 | environmentKey: process.env.NEXT_PUBLIC_FEATURE_FLAGS_ENVIRONMENT_KEY!, 10 | }; 11 | 12 | public static getInstance(): FlagsSDK { 13 | if (!ServerFlagsSDK.instance) { 14 | ServerFlagsSDK.instance = new FlagsSDK(ServerFlagsSDK.config); 15 | } 16 | return ServerFlagsSDK.instance; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + TS 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "~5.6.2", 13 | "vite": "^6.0.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import typescriptLogo from "./typescript.svg"; 3 | import viteLogo from "/vite.svg"; 4 | import { setupCounter } from "./counter.ts"; 5 | 6 | document.querySelector("#app")!.innerHTML = ` 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Vite + TypeScript 15 | 16 | 17 | 18 | 19 | Click on the Vite and TypeScript logos to learn more 20 | 21 | 22 | `; 23 | 24 | setupCounter(document.querySelector("#counter")!); 25 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/examples/vanilla-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/sdks/flags-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "sourceMap": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "preserveSymlinks": true 19 | }, 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "strictNullChecks": true 20 | }, 21 | "include": ["src", "next-env.d.ts"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": [ 7 | "base.json", 8 | "nextjs.json", 9 | "react-library.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "preserveSymlinks": true, 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "ES6", 10 | "jsx": "react-jsx" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/components/Banners/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Banner, BannerProps } from "@basestack/design-system"; 3 | import { useTheme } from "styled-components"; 4 | 5 | export interface BannersItem extends BannerProps { 6 | isVisible: boolean; 7 | } 8 | 9 | export interface BannersProps { 10 | data: Array; 11 | onDismiss?: () => void; 12 | } 13 | 14 | const Banners = ({ data, onDismiss }: BannersProps) => { 15 | const theme = useTheme(); 16 | 17 | return data.map((item, index, { length }) => 18 | item.isVisible ? ( 19 | 29 | ) : null, 30 | ); 31 | }; 32 | export default Banners; 33 | -------------------------------------------------------------------------------- /packages/ui/components/CodeLanguageCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { rem } from "polished"; 3 | 4 | export const CardButton = styled.button.withConfig({ 5 | shouldForwardProp: (prop) => prop !== "isSelected", 6 | })<{ isSelected: boolean }>` 7 | display: flex; 8 | align-items: center; 9 | cursor: pointer; 10 | padding: ${({ theme }) => theme.spacing.s3}; 11 | box-shadow: ${({ theme }) => theme.shadow.elevation2}; 12 | background-color: ${({ theme }) => 13 | theme.colors[theme.isDarkMode ? "gray800" : "white"]}; 14 | border-radius: ${rem("6px")}; 15 | border: 1px solid 16 | ${({ isSelected, theme }) => 17 | isSelected 18 | ? theme.colors[theme.isDarkMode ? "blue300" : "primary"] 19 | : theme.colors[theme.isDarkMode ? "gray800" : "white"]}; 20 | transition: box-shadow 0.2s ease-in-out; 21 | 22 | &:hover { 23 | box-shadow: ${({ theme }) => theme.shadow.elevation3}; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /packages/ui/components/HistoryCard/types.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProps } from "styled-system"; 2 | 3 | export type HistoryType = 4 | | "deleted" 5 | | "edited" 6 | | "created" 7 | | "toggledOn" 8 | | "toggledOff" 9 | | "createdProject"; 10 | 11 | export interface HistoryCardProps extends SpaceProps { 12 | avatar: string; 13 | userName: string; 14 | description: string; 15 | flagName: string; 16 | date: string; 17 | environments: Array<{ name: string; enabled: boolean }>; 18 | type: HistoryType; 19 | hasPaddingTop?: boolean; 20 | hasPaddingBottom?: boolean; 21 | hasLeftLine?: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /packages/ui/components/NotFound/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | const flexColumn = css` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const Container = styled.section` 9 | ${flexColumn}; 10 | min-height: 100vh; 11 | `; 12 | 13 | export const ContentContainer = styled.div` 14 | ${flexColumn}; 15 | justify-content: center; 16 | align-items: center; 17 | flex-grow: 1; 18 | max-width: 1440px; 19 | width: 100%; 20 | margin: 0 auto; 21 | padding: ${({ theme }) => theme.spacing.s6}; 22 | text-align: center; 23 | `; 24 | -------------------------------------------------------------------------------- /packages/ui/components/Plans/UpgradePlanHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | ${space}; 8 | `; 9 | 10 | export const Content = styled.div` 11 | display: flex; 12 | align-items: center; 13 | justify-content: space-between; 14 | margin-bottom: ${({ theme }) => theme.spacing.s4}; 15 | gap: ${({ theme }) => theme.spacing.s4}; 16 | flex-wrap: wrap; 17 | `; 18 | 19 | export const LeftContent = styled.div` 20 | display: flex; 21 | align-items: center; 22 | gap: ${({ theme }) => theme.spacing.s3}; 23 | flex-wrap: wrap; 24 | row-gap: ${({ theme }) => theme.spacing.s2}; 25 | `; 26 | 27 | export const StyledLink = styled.a` 28 | display: flex; 29 | align-items: center; 30 | text-decoration: none; 31 | color: ${({ theme }) => 32 | theme.colors[theme.isDarkMode ? "blue300" : "primary"]}; 33 | cursor: pointer; 34 | 35 | &:hover { 36 | text-decoration: underline; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /packages/ui/components/Plans/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const List = styled.ul` 9 | display: flex; 10 | flex-direction: column; 11 | gap: ${({ theme }) => theme.spacing.s3}; 12 | `; 13 | 14 | export const ListItem = styled.li` 15 | display: flex; 16 | flex-direction: column; 17 | `; 18 | -------------------------------------------------------------------------------- /packages/ui/components/Plans/types.ts: -------------------------------------------------------------------------------- 1 | export type BillingInterval = "monthly" | "yearly"; 2 | -------------------------------------------------------------------------------- /packages/ui/components/ProfileAvatarCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | padding: ${({ theme }) => theme.spacing.s5}; 7 | `; 8 | 9 | export const ContentContainer = styled.div` 10 | display: flex; 11 | align-items: center; 12 | `; 13 | 14 | export const UserDetailsContainer = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | margin: 0 ${({ theme }) => theme.spacing.s4}; 18 | `; 19 | 20 | export const Footer = styled.div` 21 | display: flex; 22 | align-items: center; 23 | padding: ${({ theme }) => theme.spacing.s4} ${({ theme }) => theme.spacing.s5}; 24 | `; 25 | 26 | export const MessageContainer = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | padding: ${({ theme }) => theme.spacing.s2}; 30 | background-color: ${({ theme }) => theme.colors.gray100}; 31 | `; 32 | -------------------------------------------------------------------------------- /packages/ui/components/ProjectsMenu/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space, SpaceProps } from "styled-system"; 3 | 4 | export const ListItem = styled.li` 5 | ${space}; 6 | `; 7 | -------------------------------------------------------------------------------- /packages/ui/components/StatusPage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | const flexColumn = css` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const Container = styled.section` 9 | ${flexColumn}; 10 | min-height: 100vh; 11 | `; 12 | 13 | const containerStyles = css` 14 | ${flexColumn}; 15 | max-width: 1440px; 16 | width: 100%; 17 | margin: 0 auto; 18 | `; 19 | 20 | export const ContentContainer = styled.div` 21 | ${containerStyles}; 22 | justify-content: center; 23 | align-items: center; 24 | flex-grow: 1; 25 | padding: ${({ theme }) => theme.spacing.s6}; 26 | text-align: center; 27 | `; 28 | 29 | export const Footer = styled.div` 30 | ${containerStyles}; 31 | align-items: center; 32 | padding: ${({ theme }) => 33 | `0 ${theme.spacing.s6} ${theme.spacing.s6} ${theme.spacing.s6}`}; 34 | `; 35 | -------------------------------------------------------------------------------- /packages/ui/components/TooltipIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SpaceProps } from "styled-system"; 3 | import { useTheme } from "styled-components"; 4 | import { 5 | Icon, 6 | Tooltip, 7 | TooltipContent, 8 | TooltipTrigger, 9 | } from "@basestack/design-system"; 10 | import { TooltipContainer } from "./styles"; 11 | 12 | interface TooltipIconProps extends SpaceProps { 13 | icon: string; 14 | text: string; 15 | } 16 | 17 | const TooltipIcon = ({ icon, text, ...props }: TooltipIconProps) => { 18 | const theme = useTheme(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 29 | 30 | {text} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export { type TooltipIconProps }; 37 | 38 | export default TooltipIcon; 39 | -------------------------------------------------------------------------------- /packages/ui/components/TooltipIcon/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { space } from "styled-system"; 3 | 4 | export const TooltipContainer = styled.div` 5 | ${space}; 6 | `; 7 | -------------------------------------------------------------------------------- /packages/ui/components/UsageCard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | gap: ${({ theme }) => theme.spacing.s2}; 8 | `; 9 | 10 | export const ContentContainer = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | `; 14 | -------------------------------------------------------------------------------- /packages/ui/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | setupFilesAfterEnv: ["/jest.setup.js"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/ui", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "AGPL-3.0", 7 | "scripts": { 8 | "test:skip": "jest", 9 | "test:update": "jest -u", 10 | "test:watch": "jest --watchAll", 11 | "test:coverage": "jest --coverage", 12 | "test:log": "jest --no-coverage >& ./logs/jest.log", 13 | "clean": "rm -rf .turbo && rm -rf node_modules" 14 | }, 15 | "devDependencies": { 16 | "@basestack/config": "*", 17 | "@basestack/tsconfig": "*", 18 | "@basestack/utils": "*", 19 | "@testing-library/jest-dom": "^6.6.3", 20 | "@types/react": "19.1.8", 21 | "@types/react-dom": "^19.1.6", 22 | "jest-styled-components": "^7.2.0", 23 | "typescript": "^5.8.3" 24 | }, 25 | "dependencies": { 26 | "@basestack/design-system": "*", 27 | "polished": "^4.2.2", 28 | "react-spring": "^10.0.1", 29 | "react-use": "^17.6.0", 30 | "styled-components": "^6.1.19", 31 | "styled-system": "^5.1.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest", "@testing-library/jest-dom"], 5 | "lib": ["ES2015", "dom"], 6 | "baseUrl": ".", 7 | "noEmit": true 8 | }, 9 | "include": ["."], 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from "@basestack/design-system/theme/lightTheme"; 2 | 3 | export type ThemeInterface = typeof theme; 4 | 5 | declare module "styled-components" { 6 | interface DefaultTheme extends ThemeInterface {} 7 | } 8 | -------------------------------------------------------------------------------- /packages/utils/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@basestack/config/eslint-preset"); 2 | -------------------------------------------------------------------------------- /packages/utils/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/utils", 3 | "version": "0.0.0", 4 | "main": "./dist/index.js", 5 | "types": "./dist/index.d.ts", 6 | "source": "./src/index.ts", 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "license": "AGPL-3.0", 11 | "scripts": { 12 | "build": "tsup src/index.ts --format esm,cjs --dts --external react --minify", 13 | "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react", 14 | "test:blocked": "jest", 15 | "test:watch": "jest --watchAll", 16 | "test:coverage": "jest --coverage", 17 | "test:log": "jest --no-coverage >& ./logs/jest.log", 18 | "lint": "TIMING=1 eslint src --fix", 19 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" 20 | }, 21 | "devDependencies": { 22 | "@basestack/config": "*", 23 | "@basestack/tsconfig": "*", 24 | "@types/uuid": "^10.0.0", 25 | "ts-jest": "^29.4.0", 26 | "tsup": "^8.5.0", 27 | "typescript": "^5.8.3" 28 | }, 29 | "dependencies": { 30 | "request-ip": "^3.3.0", 31 | "uuid": "^11.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/utils/src/config/flags.ts: -------------------------------------------------------------------------------- 1 | // Cookie based flags this is for testing purposes 2 | export const cookies = { 3 | useBilling: "useBilling", 4 | }; 5 | -------------------------------------------------------------------------------- /packages/utils/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as defaults from "./defaults"; 2 | import * as plans from "./plans"; 3 | import * as flags from "./flags"; 4 | 5 | const isDev = process.env.NODE_ENV === "development"; 6 | 7 | const config = { 8 | isDev, 9 | ...defaults, 10 | ...plans, 11 | ...flags, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/auth.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | 3 | export function withRoles(role: string, roles: string[] = ["ADMIN"]) { 4 | return function Promise>(promise: T): T { 5 | return async function ( 6 | this: unknown, 7 | ...args: Parameters 8 | ): Promise> { 9 | if (roles.includes(role)) { 10 | return promise.apply(this, args); 11 | } else { 12 | throw new TRPCError({ 13 | code: "UNAUTHORIZED", 14 | message: "You are not authorized to do this action.", 15 | }); 16 | } 17 | } as T; 18 | }; 19 | } 20 | 21 | // This is just a concept, it's not used in the codebase, needs some work and testing 22 | export const verifySignature = async (req: Request, secret: string) => { 23 | const authHeader = req.headers.get("authorization"); 24 | return !(!secret || authHeader !== `Bearer ${secret}`); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/formatters.ts: -------------------------------------------------------------------------------- 1 | export const slugify = (text: string) => 2 | text 3 | .toString() 4 | .normalize("NFD") 5 | .replace(/[\u0300-\u036f]/g, "") 6 | .toLowerCase() 7 | .trim() 8 | .replace(/\s+/g, "-") 9 | .replace(/[^\w-]+/g, "") 10 | .replace(/--+/g, "-"); 11 | 12 | export const truncateString = (text: string, maxLength: number): string => 13 | text.length <= maxLength ? text : `${text.substring(0, maxLength - 3)}...`; 14 | 15 | export const formatNumber = ( 16 | number: number, 17 | locale: string = "en-US", 18 | minimumFractionDigits?: number, 19 | maximumFractionDigits?: number, 20 | ) => { 21 | return new Intl.NumberFormat(locale, { 22 | minimumFractionDigits, 23 | maximumFractionDigits, 24 | }).format(number); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/generators.ts: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { randomBytes, createHash } from "crypto"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | /** 6 | * Generates a secure token using a combination of a random UUID, timestamp, and a random string 7 | * @returns {string} The generated secure token 8 | */ 9 | export const generateSecureToken = () => { 10 | const randomUuid = uuidv4(); 11 | const timestamp = Date.now().toString(); 12 | const randomString = randomBytes(32).toString("hex"); 13 | 14 | const combinedString = `${randomUuid}-${timestamp}-${randomString}`; 15 | return createHash("sha256").update(combinedString).digest("hex"); 16 | }; 17 | 18 | /** 19 | * Generates a unique ID from an email address using MD5 hashing 20 | * @param {string} email - The email address to generate an ID from 21 | * @returns {string} The generated unique ID 22 | */ 23 | export const emailToId = (email: string): string => { 24 | return createHash("md5").update(email.trim().toLowerCase()).digest("hex"); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/requestError.ts: -------------------------------------------------------------------------------- 1 | class RequestError extends Error { 2 | code: number; 3 | url: string; 4 | 5 | constructor({ 6 | code, 7 | url, 8 | message, 9 | }: { 10 | code: number; 11 | url: string; 12 | message: string; 13 | }) { 14 | super(message); 15 | this.code = code; 16 | this.url = url; 17 | 18 | this.name = "RequestError"; 19 | 20 | if (Error.captureStackTrace) { 21 | Error.captureStackTrace(this, RequestError); 22 | } 23 | } 24 | } 25 | 26 | export default RequestError; 27 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/table.ts: -------------------------------------------------------------------------------- 1 | export function createTable( 2 | data: Array, 3 | headers: Array, 4 | cols: (item: TData, index: number, length: number) => Array, 5 | more: (item: TData, index: number, length: number) => Array, 6 | copy?: (item: TData) => { 7 | textToCopy: string; 8 | defaultText: string; 9 | successText: string; 10 | }, 11 | ) { 12 | if (!!data) { 13 | const rows = data.map((item, index, { length }) => { 14 | const row: { 15 | more: Array; 16 | cols: Array; 17 | tooltip?: { 18 | textToCopy: string; 19 | defaultText: string; 20 | successText: string; 21 | }; 22 | } = { 23 | cols: cols(item, index, length), 24 | more: more(item, index, length), 25 | ...(copy ? { tooltip: copy(item) } : {}), 26 | }; 27 | 28 | return row; 29 | }); 30 | 31 | return { headers, rows }; 32 | } 33 | 34 | return { headers, rows: [] }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/utils/src/helpers/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * UTILS OPERATIONS 3 | */ 4 | 5 | export function getBaseUrl() { 6 | if (typeof window !== "undefined") { 7 | return ""; 8 | } 9 | // reference for vercel.com 10 | if (process.env.VERCEL_URL) { 11 | return `https://${process.env.VERCEL_URL}`; 12 | } 13 | 14 | // // reference for render.com 15 | if (process.env.RENDER_INTERNAL_HOSTNAME) { 16 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 17 | } 18 | 19 | // assume localhost 20 | return `http://localhost:${process.env.PORT ?? 3000}`; 21 | } 22 | 23 | export const getBrowserUrl = () => 24 | typeof window !== "undefined" ? window.location.origin : "localhost"; 25 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | // Helpers 2 | export * from "./helpers/browser"; 3 | export * from "./helpers/formatters"; 4 | export * from "./helpers/auth"; 5 | export * from "./helpers/url"; 6 | export * from "./helpers/table"; 7 | export * from "./helpers/validators"; 8 | export * from "./helpers/getters"; 9 | export * from "./helpers/generators"; 10 | export { default as RequestError } from "./helpers/requestError"; 11 | // Configs 12 | export { default as config } from "./config"; 13 | // Types 14 | export * from "./types"; 15 | // Middleware 16 | export * from "./middleware/cors"; 17 | export * from "./middleware/headers"; 18 | -------------------------------------------------------------------------------- /packages/utils/src/middleware/cors/index.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | export const checkCors = ( 5 | req: NextApiRequest, 6 | res: NextApiResponse, 7 | fn: Function, 8 | ) => { 9 | return new Promise((resolve, reject) => { 10 | fn(req, res, (result: any) => { 11 | if (result instanceof Error) { 12 | return reject(result); 13 | } 14 | 15 | return resolve(result); 16 | }); 17 | }); 18 | }; 19 | 20 | export const withCors = ( 21 | cors: Function, 22 | handler: (req: NextApiRequest, res: NextApiResponse) => void, 23 | ) => { 24 | return async (req: NextApiRequest, res: NextApiResponse): Promise => { 25 | try { 26 | await checkCors(req, res, cors); 27 | 28 | return handler(req, res); 29 | } catch { 30 | res.status(403).json({ error: "CORS Error" }); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/utils/src/middleware/headers/index.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | export const withHeaders = ( 5 | requiredHeaders: string[], 6 | handler: (req: NextApiRequest, res: NextApiResponse) => void, 7 | ) => { 8 | return async (req: NextApiRequest, res: NextApiResponse): Promise => { 9 | const missingHeaders: string[] = requiredHeaders.filter( 10 | (header) => !req.headers[header], 11 | ); 12 | 13 | if (missingHeaders.length > 0) { 14 | return res.status(400).json({ 15 | error: `Missing headers: ${missingHeaders.join(", ")}`, 16 | }); 17 | } 18 | 19 | return handler(req, res); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "lib": ["es5", "es6", "dom"] 6 | }, 7 | "include": ["."], 8 | "exclude": ["dist", "build", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/vendors/index.ts: -------------------------------------------------------------------------------- 1 | // QStash 2 | import * as qstash from "./src/qstash"; 3 | // Cloudflare 4 | import * as ai from "./src/cf/ai"; 5 | // Polar 6 | import * as polar from "./src/polar"; 7 | // Redis 8 | import * as redis from "./src/redis"; 9 | // Auth 10 | import * as auth from "./src/auth"; 11 | // Types 12 | export * from "./src/qstash/types"; 13 | 14 | export { qstash, ai, polar, redis, auth }; 15 | -------------------------------------------------------------------------------- /packages/vendors/jest.config.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.base.config"); 2 | 3 | module.exports = { 4 | ...common, 5 | setupFilesAfterEnv: ["/jest.setup.js"], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/vendors/jest.setup.js: -------------------------------------------------------------------------------- 1 | const common = require("@basestack/config/jest.setup.js"); 2 | 3 | module.exports = common; 4 | -------------------------------------------------------------------------------- /packages/vendors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@basestack/vendors", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "AGPL-3.0", 7 | "scripts": { 8 | "test:skip": "jest", 9 | "test:update": "jest -u", 10 | "test:watch": "jest --watchAll", 11 | "test:coverage": "jest --coverage", 12 | "test:log": "jest --no-coverage >& ./logs/jest.log", 13 | "clean": "rm -rf .turbo && rm -rf node_modules" 14 | }, 15 | "devDependencies": { 16 | "@basestack/config": "*", 17 | "@basestack/tsconfig": "*", 18 | "@basestack/utils": "*", 19 | "@testing-library/jest-dom": "^6.6.3", 20 | "@types/react": "19.1.8", 21 | "@types/react-dom": "^19.1.6", 22 | "jest-styled-components": "^7.2.0", 23 | "typescript": "^5.8.3" 24 | }, 25 | "dependencies": { 26 | "@polar-sh/sdk": "^0.34.2", 27 | "@upstash/qstash": "^2.8.1", 28 | "@upstash/redis": "^1.35.0", 29 | "@upstash/workflow": "^0.2.14", 30 | "better-auth": "^1.2.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vendors/src/auth/client.ts: -------------------------------------------------------------------------------- 1 | // Auth 2 | import { createAuthClient } from "better-auth/react"; 3 | // Plugins 4 | import { multiSessionClient } from "better-auth/client/plugins"; 5 | 6 | export const client = createAuthClient({ 7 | plugins: [multiSessionClient()], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/vendors/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | // Server 2 | import { createAuthServer } from "./server"; 3 | // Client 4 | import { client } from "./client"; 5 | 6 | export { createAuthServer, client }; 7 | -------------------------------------------------------------------------------- /packages/vendors/src/qstash/client.ts: -------------------------------------------------------------------------------- 1 | // Client 2 | import { Client } from "@upstash/workflow"; 3 | 4 | export const client = new Client({ 5 | token: process.env.QSTASH_TOKEN ?? "dummy-token-for-development", 6 | }); 7 | 8 | export const baseUrl = process.env.UPSTASH_WORKFLOW_URL; 9 | -------------------------------------------------------------------------------- /packages/vendors/src/qstash/index.ts: -------------------------------------------------------------------------------- 1 | // Events 2 | import { 3 | checkDataForSpamEvent, 4 | sendDataToExternalWebhookEvent, 5 | sendEmailEvent, 6 | } from "./events"; 7 | // Jobs 8 | import { CheckSpamJob } from "./jobs/check-spam"; 9 | import { CheckSubscriptionJob } from "./jobs/check-subscription"; 10 | import { SendDataToExternalWebHookJob } from "./jobs/send-data-to-external-webhook"; 11 | 12 | const events = { 13 | checkDataForSpamEvent, 14 | sendDataToExternalWebhookEvent, 15 | sendEmailEvent, 16 | }; 17 | 18 | const jobs = { 19 | CheckSpamJob, 20 | CheckSubscriptionJob, 21 | SendDataToExternalWebHookJob, 22 | }; 23 | 24 | export { events, jobs }; 25 | -------------------------------------------------------------------------------- /packages/vendors/src/qstash/types.ts: -------------------------------------------------------------------------------- 1 | export interface UsagePayload { 2 | externalCustomerId?: string; 3 | } 4 | 5 | export interface CheckDataForSpamPayload extends UsagePayload { 6 | userId: string; 7 | submissionId: string; 8 | data: any; 9 | } 10 | 11 | export interface SendEmailPayload extends UsagePayload { 12 | to: string[]; 13 | subject: string; 14 | template: string; 15 | props?: any; 16 | } 17 | 18 | export interface SendDataToExternalWebhookPayload extends UsagePayload { 19 | url: string; 20 | body: any; 21 | } 22 | -------------------------------------------------------------------------------- /packages/vendors/src/redis/index.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis"; 2 | 3 | // Only initialize Redis client on the server side 4 | export const client = 5 | typeof window === "undefined" 6 | ? new Redis({ 7 | url: process.env.UPSTASH_REDIS_REST_URL!, 8 | token: process.env.UPSTASH_REDIS_REST_TOKEN!, 9 | }) 10 | : null; 11 | -------------------------------------------------------------------------------- /packages/vendors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@basestack/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest", "@testing-library/jest-dom", "react"], 5 | "lib": ["ES2015", "dom"], 6 | "baseUrl": ".", 7 | "noEmit": true 8 | }, 9 | "include": ["."], 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | --------------------------------------------------------------------------------
41 | pill 42 |
19 | Click on the Vite and TypeScript logos to learn more 20 |