B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

78
config/app.config.ts Normal file
View File

@@ -0,0 +1,78 @@
import { z } from 'zod';
const production = process.env.NODE_ENV === 'production';
const AppConfigSchema = z
.object({
name: z
.string({
description: `This is the name of your SaaS. Ex. "Makerkit"`,
required_error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
})
.min(1),
title: z
.string({
description: `This is the default title tag of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
})
.min(1),
description: z.string({
description: `This is the default description of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
}),
url: z
.string({
required_error: `Please provide the variable NEXT_PUBLIC_SITE_URL`,
})
.url({
message: `You are deploying a production build but have entered a NEXT_PUBLIC_SITE_URL variable using http instead of https. It is very likely that you have set the incorrect URL. The build will now fail to prevent you from from deploying a faulty configuration. Please provide the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`,
}),
locale: z
.string({
description: `This is the default locale of your SaaS.`,
required_error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
})
.default('en'),
theme: z.enum(['light', 'dark', 'system']),
production: z.boolean(),
themeColor: z.string(),
themeColorDark: z.string(),
})
.refine(
(schema) => {
const isCI = process.env.NEXT_PUBLIC_CI;
if (isCI ?? !schema.production) {
return true;
}
return !schema.url.startsWith('http:');
},
{
message: `Please provide a valid HTTPS URL. Set the variable NEXT_PUBLIC_SITE_URL with a valid URL, such as: 'https://example.com'`,
path: ['url'],
},
)
.refine(
(schema) => {
return schema.themeColor !== schema.themeColorDark;
},
{
message: `Please provide different theme colors for light and dark themes.`,
path: ['themeColor'],
},
);
const appConfig = AppConfigSchema.parse({
name: process.env.NEXT_PUBLIC_PRODUCT_NAME,
title: process.env.NEXT_PUBLIC_SITE_TITLE,
description: process.env.NEXT_PUBLIC_SITE_DESCRIPTION,
url: process.env.NEXT_PUBLIC_SITE_URL,
locale: process.env.NEXT_PUBLIC_DEFAULT_LOCALE,
theme: process.env.NEXT_PUBLIC_DEFAULT_THEME_MODE,
themeColor: process.env.NEXT_PUBLIC_THEME_COLOR,
themeColorDark: process.env.NEXT_PUBLIC_THEME_COLOR_DARK,
production,
});
export default appConfig;

73
config/auth.config.ts Normal file
View File

@@ -0,0 +1,73 @@
import type { Provider } from '@supabase/supabase-js';
import { z } from 'zod';
const providers: z.ZodType<Provider> = getProviders();
const AuthConfigSchema = z.object({
captchaTokenSiteKey: z
.string({
description: 'The reCAPTCHA site key.',
})
.optional(),
displayTermsCheckbox: z
.boolean({
description: 'Whether to display the terms checkbox during sign-up.',
})
.optional(),
providers: z.object({
password: z.boolean({
description: 'Enable password authentication.',
}),
magicLink: z.boolean({
description: 'Enable magic link authentication.',
}),
oAuth: providers.array(),
}),
});
const authConfig = AuthConfigSchema.parse({
// NB: This is a public key, so it's safe to expose.
// Copy the value from the Supabase Dashboard.
captchaTokenSiteKey: process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY,
// whether to display the terms checkbox during sign-up
displayTermsCheckbox:
process.env.NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX === 'true',
// NB: Enable the providers below in the Supabase Console
// in your production project
providers: {
password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
magicLink: process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true',
oAuth: ['google'],
},
} satisfies z.infer<typeof AuthConfigSchema>);
export default authConfig;
function getProviders() {
return z.enum([
'apple',
'azure',
'bitbucket',
'discord',
'facebook',
'figma',
'github',
'gitlab',
'google',
'kakao',
'keycloak',
'linkedin',
'linkedin_oidc',
'notion',
'slack',
'spotify',
'twitch',
'twitter',
'workos',
'zoom',
'fly',
]);
}

8
config/billing.config.ts Normal file
View File

@@ -0,0 +1,8 @@
/*
Replace this file with your own billing configuration file.
Copy it from billing.sample.config.ts and update the configuration to match your billing provider and products.
This file will never be overwritten by git updates
*/
import sampleSchema from './billing.sample.config';
export default sampleSchema;

View File

@@ -0,0 +1,148 @@
/**
* This is a sample billing configuration file. You should copy this file to `billing.config.ts` and then replace
* the configuration with your own billing provider and products.
*/
import { BillingProviderSchema, createBillingSchema } from '@kit/billing';
// The billing provider to use. This should be set in the environment variables
// and should match the provider in the database. We also add it here so we can validate
// your configuration against the selected provider at build time.
const provider = BillingProviderSchema.parse(
process.env.NEXT_PUBLIC_BILLING_PROVIDER,
);
export default createBillingSchema({
// also update config.billing_provider in the DB to match the selected
provider,
// products configuration
products: [
{
id: 'starter',
name: 'Starter',
description: 'The perfect plan to get started',
currency: 'USD',
badge: `Value`,
plans: [
{
name: 'Starter Monthly',
id: 'starter-monthly',
paymentType: 'recurring',
interval: 'month',
lineItems: [
{
id: 'price_1NNwYHI1i3VnbZTqI2UzaHIe',
name: 'Starter',
cost: 9.99,
type: 'flat' as const,
},
],
},
{
name: 'Starter Yearly',
id: 'starter-yearly',
paymentType: 'recurring',
interval: 'year',
lineItems: [
{
id: 'starter-yearly',
name: 'Base',
cost: 99.99,
type: 'flat' as const,
},
],
},
],
features: ['Feature 1', 'Feature 2', 'Feature 3'],
},
{
id: 'pro',
name: 'Pro',
badge: `Popular`,
highlighted: true,
description: 'The perfect plan for professionals',
currency: 'USD',
plans: [
{
name: 'Pro Monthly',
id: 'pro-monthly',
paymentType: 'recurring',
interval: 'month',
lineItems: [
{
id: 'price_1PGOAVI1i3VnbZTqc69xaypm',
name: 'Base',
cost: 19.99,
type: 'flat',
},
],
},
{
name: 'Pro Yearly',
id: 'pro-yearly',
paymentType: 'recurring',
interval: 'year',
lineItems: [
{
id: 'price_pro_yearly',
name: 'Base',
cost: 199.99,
type: 'flat',
},
],
},
],
features: [
'Feature 1',
'Feature 2',
'Feature 3',
'Feature 4',
'Feature 5',
],
},
{
id: 'enterprise',
name: 'Enterprise',
description: 'The perfect plan for enterprises',
currency: 'USD',
plans: [
{
name: 'Enterprise Monthly',
id: 'enterprise-monthly',
paymentType: 'recurring',
interval: 'month',
lineItems: [
{
id: 'price_enterprise-monthly',
name: 'Base',
cost: 29.99,
type: 'flat',
},
],
},
{
name: 'Enterprise Yearly',
id: 'enterprise-yearly',
paymentType: 'recurring',
interval: 'year',
lineItems: [
{
id: 'price_enterprise_yearly',
name: 'Base',
cost: 299.9,
type: 'flat',
},
],
},
],
features: [
'Feature 1',
'Feature 2',
'Feature 3',
'Feature 4',
'Feature 5',
'Feature 6',
'Feature 7',
],
},
],
});

View File

@@ -0,0 +1,112 @@
import { z } from 'zod';
type LanguagePriority = 'user' | 'application';
const FeatureFlagsSchema = z.object({
enableThemeToggle: z.boolean({
description: 'Enable theme toggle in the user interface.',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
}),
enableAccountDeletion: z.boolean({
description: 'Enable personal account deletion.',
required_error:
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
}),
enableTeamDeletion: z.boolean({
description: 'Enable team deletion.',
required_error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
}),
enableTeamAccounts: z.boolean({
description: 'Enable team accounts.',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
}),
enableTeamCreation: z.boolean({
description: 'Enable team creation.',
required_error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
}),
enablePersonalAccountBilling: z.boolean({
description: 'Enable personal account billing.',
required_error:
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
}),
enableTeamAccountBilling: z.boolean({
description: 'Enable team account billing.',
required_error:
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
}),
languagePriority: z
.enum(['user', 'application'], {
required_error: 'Provide the variable NEXT_PUBLIC_LANGUAGE_PRIORITY',
description: `If set to user, use the user's preferred language. If set to application, use the application's default language.`,
})
.default('application'),
enableNotifications: z.boolean({
description: 'Enable notifications functionality',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
}),
realtimeNotifications: z.boolean({
description: 'Enable realtime for the notifications functionality',
required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
}),
enableVersionUpdater: z.boolean({
description: 'Enable version updater',
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
}),
});
const featuresFlagConfig = FeatureFlagsSchema.parse({
enableThemeToggle: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_THEME_TOGGLE,
true,
),
enableAccountDeletion: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION,
false,
),
enableTeamDeletion: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION,
false,
),
enableTeamAccounts: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS,
true,
),
enableTeamCreation: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION,
true,
),
enablePersonalAccountBilling: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING,
false,
),
enableTeamAccountBilling: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING,
false,
),
languagePriority: process.env
.NEXT_PUBLIC_LANGUAGE_PRIORITY as LanguagePriority,
enableNotifications: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_NOTIFICATIONS,
true,
),
realtimeNotifications: getBoolean(
process.env.NEXT_PUBLIC_REALTIME_NOTIFICATIONS,
false,
),
enableVersionUpdater: getBoolean(
process.env.NEXT_PUBLIC_ENABLE_VERSION_UPDATER,
false,
),
} satisfies z.infer<typeof FeatureFlagsSchema>);
export default featuresFlagConfig;
function getBoolean(value: unknown, defaultValue: boolean) {
if (typeof value === 'string') {
return value === 'true';
}
return defaultValue;
}

49
config/paths.config.ts Normal file
View File

@@ -0,0 +1,49 @@
import { z } from 'zod';
const PathsSchema = z.object({
auth: z.object({
signIn: z.string().min(1),
signUp: z.string().min(1),
verifyMfa: z.string().min(1),
callback: z.string().min(1),
passwordReset: z.string().min(1),
passwordUpdate: z.string().min(1),
}),
app: z.object({
home: z.string().min(1),
personalAccountSettings: z.string().min(1),
personalAccountBilling: z.string().min(1),
personalAccountBillingReturn: z.string().min(1),
accountHome: z.string().min(1),
accountSettings: z.string().min(1),
accountBilling: z.string().min(1),
accountMembers: z.string().min(1),
accountBillingReturn: z.string().min(1),
joinTeam: z.string().min(1),
}),
});
const pathsConfig = PathsSchema.parse({
auth: {
signIn: '/auth/sign-in',
signUp: '/auth/sign-up',
verifyMfa: '/auth/verify',
callback: '/auth/callback',
passwordReset: '/auth/password-reset',
passwordUpdate: '/update-password',
},
app: {
home: '/home',
personalAccountSettings: '/home/settings',
personalAccountBilling: '/home/billing',
personalAccountBillingReturn: '/home/billing/return',
accountHome: '/home/[account]',
accountSettings: `/home/[account]/settings`,
accountBilling: `/home/[account]/billing`,
accountMembers: `/home/[account]/members`,
accountBillingReturn: `/home/[account]/billing/return`,
joinTeam: '/join',
},
} satisfies z.infer<typeof PathsSchema>);
export default pathsConfig;

View File

@@ -0,0 +1,47 @@
import { CreditCard, Home, User } from 'lucide-react';
import { z } from 'zod';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const iconClasses = 'w-4';
const routes = [
{
label: 'common:routes.application',
children: [
{
label: 'common:routes.home',
path: pathsConfig.app.home,
Icon: <Home className={iconClasses} />,
end: true,
},
],
},
{
label: 'common:routes.settings',
children: [
{
label: 'common:routes.profile',
path: pathsConfig.app.personalAccountSettings,
Icon: <User className={iconClasses} />,
},
featureFlagsConfig.enablePersonalAccountBilling
? {
label: 'common:routes.billing',
path: pathsConfig.app.personalAccountBilling,
Icon: <CreditCard className={iconClasses} />,
}
: undefined,
].filter((route) => !!route),
},
] satisfies z.infer<typeof NavigationConfigSchema>['routes'];
export const personalAccountNavigationConfig = NavigationConfigSchema.parse({
routes,
style: process.env.NEXT_PUBLIC_USER_NAVIGATION_STYLE,
sidebarCollapsed: process.env.NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED,
sidebarCollapsedStyle: process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSED_STYLE,
});

View File

@@ -0,0 +1,58 @@
import { CreditCard, LayoutDashboard, Settings, Users } from 'lucide-react';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
const iconClasses = 'w-4';
const getRoutes = (account: string) => [
{
label: 'common:routes.application',
children: [
{
label: 'common:routes.dashboard',
path: pathsConfig.app.accountHome.replace('[account]', account),
Icon: <LayoutDashboard className={iconClasses} />,
end: true,
},
],
},
{
label: 'common:routes.settings',
collapsible: false,
children: [
{
label: 'common:routes.settings',
path: createPath(pathsConfig.app.accountSettings, account),
Icon: <Settings className={iconClasses} />,
},
{
label: 'common:routes.members',
path: createPath(pathsConfig.app.accountMembers, account),
Icon: <Users className={iconClasses} />,
},
featureFlagsConfig.enableTeamAccountBilling
? {
label: 'common:routes.billing',
path: createPath(pathsConfig.app.accountBilling, account),
Icon: <CreditCard className={iconClasses} />,
}
: undefined,
].filter(Boolean),
},
];
export function getTeamAccountSidebarConfig(account: string) {
return NavigationConfigSchema.parse({
routes: getRoutes(account),
style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE,
sidebarCollapsed: process.env.NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED,
sidebarCollapsedStyle: process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSED_STYLE,
});
}
function createPath(path: string, account: string) {
return path.replace('[account]', account);
}