Merge remote-tracking branch 'origin/main' into cloudflare

This commit is contained in:
javayhu 2025-08-15 23:03:55 +08:00
commit 63dd4e52fb
19 changed files with 208 additions and 132 deletions

View File

@ -2,6 +2,7 @@
import { getDb } from '@/db'; import { getDb } from '@/db';
import { user } from '@/db/schema'; import { user } from '@/db/schema';
import { isDemoWebsite } from '@/lib/demo';
import { asc, desc, ilike, or, sql } from 'drizzle-orm'; import { asc, desc, ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action'; import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod'; import { z } from 'zod';
@ -75,7 +76,8 @@ export const getUsersAction = actionClient
]); ]);
// hide user data in demo website // hide user data in demo website
if (process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true') { const isDemo = isDemoWebsite();
if (isDemo) {
items = items.map((item) => ({ items = items.map((item) => ({
...item, ...item,
name: 'Demo User', name: 'Demo User',

View File

@ -1,4 +1,5 @@
import { DashboardHeader } from '@/components/dashboard/dashboard-header'; import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { isDemoWebsite } from '@/lib/demo';
import { getSession } from '@/lib/server'; import { getSession } from '@/lib/server';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -9,7 +10,7 @@ interface UsersLayoutProps {
export default async function UsersLayout({ children }: UsersLayoutProps) { export default async function UsersLayout({ children }: UsersLayoutProps) {
// if is demo website, allow user to access admin and user pages, but data is fake // if is demo website, allow user to access admin and user pages, but data is fake
const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; const isDemo = isDemoWebsite();
// Check if user is admin // Check if user is admin
const session = await getSession(); const session = await getSession();
if (!session || (session.user.role !== 'admin' && !isDemo)) { if (!session || (session.user.role !== 'admin' && !isDemo)) {

View File

@ -1,20 +1,15 @@
import { UpdateAvatarCard } from '@/components/settings/profile/update-avatar-card'; import { UpdateAvatarCard } from '@/components/settings/profile/update-avatar-card';
import { UpdateNameCard } from '@/components/settings/profile/update-name-card'; import { UpdateNameCard } from '@/components/settings/profile/update-name-card';
import { websiteConfig } from '@/config/website';
export default function ProfilePage() { export default function ProfilePage() {
const enableUpdateAvatar = websiteConfig.features.enableUpdateAvatar;
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{enableUpdateAvatar && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<UpdateAvatarCard />
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<UpdateNameCard /> <UpdateNameCard />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<UpdateAvatarCard />
</div>
</div> </div>
); );
} }

View File

@ -14,8 +14,6 @@ type Href = Parameters<typeof getLocalePathname>[0]['href'];
const staticRoutes = [ const staticRoutes = [
'/', '/',
'/pricing', '/pricing',
'/blog',
'/docs',
'/about', '/about',
'/contact', '/contact',
'/waitlist', '/waitlist',
@ -25,6 +23,8 @@ const staticRoutes = [
'/cookie', '/cookie',
'/auth/login', '/auth/login',
'/auth/register', '/auth/register',
...(websiteConfig.blog.enable ? ['/blog'] : []),
...(websiteConfig.docs.enable ? ['/docs'] : []),
]; ];
/** /**
@ -48,101 +48,106 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}) })
); );
// add categories // add blog related routes if enabled
sitemapList.push( if (websiteConfig.blog.enable) {
...categorySource.getPages().flatMap((category) => // add categories
routing.locales.map((locale) => ({ sitemapList.push(
url: getUrl(`/blog/category/${category.slugs[0]}`, locale), ...categorySource.getPages().flatMap((category) =>
lastModified: new Date(), routing.locales.map((locale) => ({
priority: 0.8, url: getUrl(`/blog/category/${category.slugs[0]}`, locale),
changeFrequency: 'weekly' as const, lastModified: new Date(),
})) priority: 0.8,
) changeFrequency: 'weekly' as const,
); }))
)
// add paginated blog list pages
routing.locales.forEach((locale) => {
const posts = blogSource
.getPages(locale)
.filter((post) => post.data.published);
const totalPages = Math.max(
1,
Math.ceil(posts.length / websiteConfig.blog.paginationSize)
); );
// /blog/page/[page] (from 2)
for (let page = 2; page <= totalPages; page++) {
sitemapList.push({
url: getUrl(`/blog/page/${page}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
});
}
});
// add paginated category pages // add paginated blog list pages
routing.locales.forEach((locale) => { routing.locales.forEach((locale) => {
const localeCategories = categorySource.getPages(locale); const posts = blogSource
localeCategories.forEach((category) => {
// posts in this category and locale
const postsInCategory = blogSource
.getPages(locale) .getPages(locale)
.filter((post) => post.data.published) .filter((post) => post.data.published);
.filter((post) =>
post.data.categories.some((cat) => cat === category.slugs[0])
);
const totalPages = Math.max( const totalPages = Math.max(
1, 1,
Math.ceil(postsInCategory.length / websiteConfig.blog.paginationSize) Math.ceil(posts.length / websiteConfig.blog.paginationSize)
); );
// /blog/category/[slug] (first page) // /blog/page/[page] (from 2)
sitemapList.push({
url: getUrl(`/blog/category/${category.slugs[0]}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
});
// /blog/category/[slug]/page/[page] (from 2)
for (let page = 2; page <= totalPages; page++) { for (let page = 2; page <= totalPages; page++) {
sitemapList.push({ sitemapList.push({
url: getUrl( url: getUrl(`/blog/page/${page}`, locale),
`/blog/category/${category.slugs[0]}/page/${page}`,
locale
),
lastModified: new Date(), lastModified: new Date(),
priority: 0.8, priority: 0.8,
changeFrequency: 'weekly' as const, changeFrequency: 'weekly' as const,
}); });
} }
}); });
});
// add posts (single post pages) // add paginated category pages
sitemapList.push( routing.locales.forEach((locale) => {
...blogSource.getPages().flatMap((post) => const localeCategories = categorySource.getPages(locale);
routing.locales localeCategories.forEach((category) => {
.filter((locale) => post.locale === locale) // posts in this category and locale
.map((locale) => ({ const postsInCategory = blogSource
url: getUrl(`/blog/${post.slugs.join('/')}`, locale), .getPages(locale)
.filter((post) => post.data.published)
.filter((post) =>
post.data.categories.some((cat) => cat === category.slugs[0])
);
const totalPages = Math.max(
1,
Math.ceil(postsInCategory.length / websiteConfig.blog.paginationSize)
);
// /blog/category/[slug] (first page)
sitemapList.push({
url: getUrl(`/blog/category/${category.slugs[0]}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
});
// /blog/category/[slug]/page/[page] (from 2)
for (let page = 2; page <= totalPages; page++) {
sitemapList.push({
url: getUrl(
`/blog/category/${category.slugs[0]}/page/${page}`,
locale
),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
});
}
});
});
// add posts (single post pages)
sitemapList.push(
...blogSource.getPages().flatMap((post) =>
routing.locales
.filter((locale) => post.locale === locale)
.map((locale) => ({
url: getUrl(`/blog/${post.slugs.join('/')}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
}))
)
);
}
// add docs related routes if enabled
if (websiteConfig.docs.enable) {
const docsParams = source.generateParams();
sitemapList.push(
...docsParams.flatMap((param) =>
routing.locales.map((locale) => ({
url: getUrl(`/docs/${param.slug.join('/')}`, locale),
lastModified: new Date(), lastModified: new Date(),
priority: 0.8, priority: 0.8,
changeFrequency: 'weekly' as const, changeFrequency: 'weekly' as const,
})) }))
) )
); );
}
// add docs
const docsParams = source.generateParams();
sitemapList.push(
...docsParams.flatMap((param) =>
routing.locales.map((locale) => ({
url: getUrl(`/docs/${param.slug.join('/')}`, locale),
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'weekly' as const,
}))
)
);
return sitemapList; return sitemapList;
} }

View File

@ -23,6 +23,7 @@ import { Textarea } from '@/components/ui/textarea';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import type { User } from '@/lib/auth-types'; import type { User } from '@/lib/auth-types';
import { isDemoWebsite } from '@/lib/demo';
import { formatDate } from '@/lib/formatter'; import { formatDate } from '@/lib/formatter';
import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls'; import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -53,7 +54,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
const triggerRefresh = useUsersStore((state) => state.triggerRefresh); const triggerRefresh = useUsersStore((state) => state.triggerRefresh);
// show fake data in demo website // show fake data in demo website
const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; const isDemo = isDemoWebsite();
const handleBan = async () => { const handleBan = async () => {
if (!banReason) { if (!banReason) {

View File

@ -27,6 +27,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import type { User } from '@/lib/auth-types'; import type { User } from '@/lib/auth-types';
import { isDemoWebsite } from '@/lib/demo';
import { formatDate } from '@/lib/formatter'; import { formatDate } from '@/lib/formatter';
import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls'; import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls';
import { IconCaretDownFilled, IconCaretUpFilled } from '@tabler/icons-react'; import { IconCaretDownFilled, IconCaretUpFilled } from '@tabler/icons-react';
@ -152,7 +153,7 @@ export function UsersTable({
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
// show fake data in demo website // show fake data in demo website
const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; const isDemo = isDemoWebsite();
// Map column IDs to translation keys // Map column IDs to translation keys
const columnIdToTranslationKey = { const columnIdToTranslationKey = {

View File

@ -7,6 +7,7 @@ import {
} from '@/components/ui/breadcrumb'; } from '@/components/ui/breadcrumb';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { SidebarTrigger } from '@/components/ui/sidebar'; import { SidebarTrigger } from '@/components/ui/sidebar';
import { isDemoWebsite } from '@/lib/demo';
import React, { type ReactNode } from 'react'; import React, { type ReactNode } from 'react';
import { CreditsBalanceButton } from '../layout/credits-balance-button'; import { CreditsBalanceButton } from '../layout/credits-balance-button';
import LocaleSwitcher from '../layout/locale-switcher'; import LocaleSwitcher from '../layout/locale-switcher';
@ -30,8 +31,7 @@ export function DashboardHeader({
breadcrumbs, breadcrumbs,
actions, actions,
}: DashboardHeaderProps) { }: DashboardHeaderProps) {
// if is demo website, allow user to access admin and user pages, but data is fake const isDemo = isDemoWebsite();
const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true';
return ( return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"> <header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">

View File

@ -19,7 +19,7 @@ interface YoutubeVideoProps {
export const YoutubeVideo = ({ export const YoutubeVideo = ({
url, url,
width = 560, width = 560,
height = 315, height = 460,
}: YoutubeVideoProps) => { }: YoutubeVideoProps) => {
return ( return (
<div className="my-4"> <div className="my-4">

View File

@ -10,7 +10,6 @@ import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import type React from 'react'; import type React from 'react';
import { ThemeSelector } from './theme-selector';
export function Footer({ className }: React.HTMLAttributes<HTMLElement>) { export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const t = useTranslations(); const t = useTranslations();
@ -46,7 +45,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
aria-label={link.title} aria-label={link.title}
className="border border-border inline-flex h-8 w-8 items-center className="border border-border inline-flex h-8 w-8 items-center
justify-center rounded-full hover:bg-accent hover:text-accent-foreground" justify-center rounded-full hover:bg-accent hover:text-accent-foreground"
> >
<span className="sr-only">{link.title}</span> <span className="sr-only">{link.title}</span>
@ -99,7 +98,6 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
</span> </span>
<div className="flex items-center gap-x-4"> <div className="flex items-center gap-x-4">
{/* <ThemeSelector /> */}
<ModeSwitcherHorizontal /> <ModeSwitcherHorizontal />
</div> </div>
</Container> </Container>

View File

@ -1,10 +1,16 @@
'use client'; 'use client';
import { NewsletterForm } from '@/components/newsletter/newsletter-form'; import { NewsletterForm } from '@/components/newsletter/newsletter-form';
import { websiteConfig } from '@/config/website';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { HeaderSection } from '../layout/header-section'; import { HeaderSection } from '../layout/header-section';
export function NewsletterCard() { export function NewsletterCard() {
// show nothing if newsletter is disabled
if (!websiteConfig.newsletter.enable) {
return null;
}
const t = useTranslations('Newsletter'); const t = useTranslations('Newsletter');
return ( return (

View File

@ -20,6 +20,7 @@ import {
FormLabel, FormLabel,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { websiteConfig } from '@/config/website';
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -40,6 +41,11 @@ interface NewsletterFormCardProps {
* Allows users to toggle their newsletter subscription status * Allows users to toggle their newsletter subscription status
*/ */
export function NewsletterFormCard({ className }: NewsletterFormCardProps) { export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
// show nothing if newsletter is disabled
if (!websiteConfig.newsletter.enable) {
return null;
}
const t = useTranslations('Dashboard.settings.notification'); const t = useTranslations('Dashboard.settings.notification');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>(''); const [error, setError] = useState<string | undefined>('');

View File

@ -11,6 +11,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card'; } from '@/components/ui/card';
import { websiteConfig } from '@/config/website';
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { uploadFileFromBrowser } from '@/storage/client'; import { uploadFileFromBrowser } from '@/storage/client';
@ -27,6 +28,14 @@ interface UpdateAvatarCardProps {
* Update the user's avatar * Update the user's avatar
*/ */
export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) { export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) {
// show nothing if storage is disabled or update avatar is disabled
if (
!websiteConfig.storage.enable ||
!websiteConfig.features.enableUpdateAvatar
) {
return null;
}
const t = useTranslations('Dashboard.settings.profile'); const t = useTranslations('Dashboard.settings.profile');
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | undefined>(''); const [error, setError] = useState<string | undefined>('');

View File

@ -3,6 +3,7 @@
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import type { NestedMenuItem } from '@/types'; import type { NestedMenuItem } from '@/types';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { websiteConfig } from './website';
/** /**
* Get footer config with translations * Get footer config with translations
@ -41,16 +42,24 @@ export function getFooterLinks(): NestedMenuItem[] {
{ {
title: t('resources.title'), title: t('resources.title'),
items: [ items: [
{ ...(websiteConfig.blog.enable
title: t('resources.items.blog'), ? [
href: Routes.Blog, {
external: false, title: t('resources.items.blog'),
}, href: Routes.Blog,
{ external: false,
title: t('resources.items.docs'), },
href: Routes.Docs, ]
external: false, : []),
}, ...(websiteConfig.docs.enable
? [
{
title: t('resources.items.docs'),
href: Routes.Docs,
external: false,
},
]
: []),
{ {
title: t('resources.items.changelog'), title: t('resources.items.changelog'),
href: Routes.Changelog, href: Routes.Changelog,

View File

@ -34,6 +34,7 @@ import {
WandSparklesIcon, WandSparklesIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { websiteConfig } from './website';
/** /**
* Get navbar config with translations * Get navbar config with translations
@ -59,16 +60,24 @@ export function getNavbarLinks(): NestedMenuItem[] {
href: Routes.Pricing, href: Routes.Pricing,
external: false, external: false,
}, },
{ ...(websiteConfig.blog.enable
title: t('blog.title'), ? [
href: Routes.Blog, {
external: false, title: t('blog.title'),
}, href: Routes.Blog,
{ external: false,
title: t('docs.title'), },
href: Routes.Docs, ]
external: false, : []),
}, ...(websiteConfig.docs.enable
? [
{
title: t('docs.title'),
href: Routes.Docs,
external: false,
},
]
: []),
{ {
title: t('ai.title'), title: t('ai.title'),
items: [ items: [

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import { isDemoWebsite } from '@/lib/demo';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import type { NestedMenuItem } from '@/types'; import type { NestedMenuItem } from '@/types';
import { import {
@ -30,7 +31,7 @@ export function getSidebarLinks(): NestedMenuItem[] {
const t = useTranslations('Dashboard'); const t = useTranslations('Dashboard');
// if is demo website, allow user to access admin and user pages, but data is fake // if is demo website, allow user to access admin and user pages, but data is fake
const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; const isDemo = isDemoWebsite();
return [ return [
{ {

View File

@ -34,12 +34,12 @@ export const websiteConfig: WebsiteConfig = {
}, },
features: { features: {
enableDiscordWidget: false, enableDiscordWidget: false,
enableCrispChat: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true',
enableUpgradeCard: true, enableUpgradeCard: true,
enableUpdateAvatar: true, enableUpdateAvatar: true,
enableAffonsoAffiliate: false, enableAffonsoAffiliate: false,
enablePromotekitAffiliate: false, enablePromotekitAffiliate: false,
enableDatafastRevenueTrack: false, enableDatafastRevenueTrack: false,
enableCrispChat: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true',
enableTurnstileCaptcha: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true', enableTurnstileCaptcha: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true',
}, },
routes: { routes: {
@ -68,19 +68,25 @@ export const websiteConfig: WebsiteConfig = {
}, },
}, },
blog: { blog: {
enable: true,
paginationSize: 6, paginationSize: 6,
relatedPostsSize: 3, relatedPostsSize: 3,
}, },
docs: {
enable: true,
},
mail: { mail: {
provider: 'resend', provider: 'resend',
fromEmail: 'MkSaaS <support@mksaas.com>', fromEmail: 'MkSaaS <support@mksaas.com>',
supportEmail: 'MkSaaS <support@mksaas.com>', supportEmail: 'MkSaaS <support@mksaas.com>',
}, },
newsletter: { newsletter: {
enable: true,
provider: 'resend', provider: 'resend',
autoSubscribeAfterSignUp: true, autoSubscribeAfterSignUp: true,
}, },
storage: { storage: {
enable: true,
provider: 's3', provider: 's3',
}, },
payment: { payment: {

View File

@ -164,7 +164,11 @@ export function getLocaleFromRequest(request?: Request): Locale {
async function onCreateUser(user: User) { async function onCreateUser(user: User) {
// Auto subscribe user to newsletter after sign up if enabled in website config // Auto subscribe user to newsletter after sign up if enabled in website config
// Add a delay to avoid hitting Resend's 1 email per second limit // Add a delay to avoid hitting Resend's 1 email per second limit
if (user.email && websiteConfig.newsletter.autoSubscribeAfterSignUp) { if (
user.email &&
websiteConfig.newsletter.enable &&
websiteConfig.newsletter.autoSubscribeAfterSignUp
) {
// Delay newsletter subscription by 2 seconds to avoid rate limiting // Delay newsletter subscription by 2 seconds to avoid rate limiting
// This ensures the email verification email is sent first // This ensures the email verification email is sent first
// Using 2 seconds instead of 1 to provide extra buffer for network delays // Using 2 seconds instead of 1 to provide extra buffer for network delays
@ -184,6 +188,7 @@ async function onCreateUser(user: User) {
// Add register gift credits to the user if enabled in website config // Add register gift credits to the user if enabled in website config
if ( if (
websiteConfig.credits.enableCredits &&
websiteConfig.credits.registerGiftCredits.enable && websiteConfig.credits.registerGiftCredits.enable &&
websiteConfig.credits.registerGiftCredits.credits > 0 websiteConfig.credits.registerGiftCredits.credits > 0
) { ) {
@ -199,21 +204,26 @@ async function onCreateUser(user: User) {
} }
// Add free monthly credits to the user if enabled in website config // Add free monthly credits to the user if enabled in website config
const pricePlans = await getAllPricePlans();
const freePlan = pricePlans.find((plan) => plan.isFree);
if ( if (
freePlan?.credits?.enable && websiteConfig.credits.enableCredits &&
freePlan?.credits?.amount && websiteConfig.credits.enableForFreePlan
freePlan?.credits?.amount > 0
) { ) {
try { const pricePlans = await getAllPricePlans();
await addMonthlyFreeCredits(user.id); const freePlan = pricePlans.find((plan) => plan.isFree);
const credits = freePlan.credits.amount; if (
console.log( freePlan?.credits?.enable &&
`added free monthly credits for user ${user.id}, credits: ${credits}` freePlan?.credits?.amount &&
); freePlan?.credits?.amount > 0
} catch (error) { ) {
console.error('Free monthly credits error:', error); try {
await addMonthlyFreeCredits(user.id);
const credits = freePlan.credits.amount;
console.log(
`added free monthly credits for user ${user.id}, credits: ${credits}`
);
} catch (error) {
console.error('Free monthly credits error:', error);
}
} }
} }
} }

6
src/lib/demo.ts Normal file
View File

@ -0,0 +1,6 @@
/**
* check if the website is a demo website
*/
export function isDemoWebsite() {
return process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true';
}

11
src/types/index.d.ts vendored
View File

@ -13,6 +13,7 @@ export type WebsiteConfig = {
auth: AuthConfig; auth: AuthConfig;
i18n: I18nConfig; i18n: I18nConfig;
blog: BlogConfig; blog: BlogConfig;
docs: DocsConfig;
mail: MailConfig; mail: MailConfig;
newsletter: NewsletterConfig; newsletter: NewsletterConfig;
storage: StorageConfig; storage: StorageConfig;
@ -111,10 +112,18 @@ export interface I18nConfig {
* Blog configuration * Blog configuration
*/ */
export interface BlogConfig { export interface BlogConfig {
enable: boolean; // Whether to enable the blog
paginationSize: number; // Number of posts per page paginationSize: number; // Number of posts per page
relatedPostsSize: number; // Number of related posts to show relatedPostsSize: number; // Number of related posts to show
} }
/**
* Docs configuration
*/
export interface DocsConfig {
enable: boolean; // Whether to enable the docs
}
/** /**
* Mail configuration * Mail configuration
*/ */
@ -128,6 +137,7 @@ export interface MailConfig {
* Newsletter configuration * Newsletter configuration
*/ */
export interface NewsletterConfig { export interface NewsletterConfig {
enable: boolean; // Whether to enable the newsletter
provider: 'resend'; // The newsletter provider, only resend is supported for now provider: 'resend'; // The newsletter provider, only resend is supported for now
autoSubscribeAfterSignUp?: boolean; // Whether to automatically subscribe users to the newsletter after sign up autoSubscribeAfterSignUp?: boolean; // Whether to automatically subscribe users to the newsletter after sign up
} }
@ -136,6 +146,7 @@ export interface NewsletterConfig {
* Storage configuration * Storage configuration
*/ */
export interface StorageConfig { export interface StorageConfig {
enable: boolean; // Whether to enable the storage
provider: 's3'; // The storage provider, only s3 is supported for now provider: 's3'; // The storage provider, only s3 is supported for now
} }