diff --git a/src/actions/get-users.ts b/src/actions/get-users.ts index afd68c7..67d3005 100644 --- a/src/actions/get-users.ts +++ b/src/actions/get-users.ts @@ -2,6 +2,7 @@ import { getDb } from '@/db'; import { user } from '@/db/schema'; +import { isDemoWebsite } from '@/lib/demo'; import { asc, desc, ilike, or, sql } from 'drizzle-orm'; import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; @@ -75,7 +76,8 @@ export const getUsersAction = actionClient ]); // hide user data in demo website - if (process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true') { + const isDemo = isDemoWebsite(); + if (isDemo) { items = items.map((item) => ({ ...item, name: 'Demo User', diff --git a/src/app/[locale]/(protected)/admin/users/layout.tsx b/src/app/[locale]/(protected)/admin/users/layout.tsx index 1bd220c..f3cffb8 100644 --- a/src/app/[locale]/(protected)/admin/users/layout.tsx +++ b/src/app/[locale]/(protected)/admin/users/layout.tsx @@ -1,4 +1,5 @@ import { DashboardHeader } from '@/components/dashboard/dashboard-header'; +import { isDemoWebsite } from '@/lib/demo'; import { getSession } from '@/lib/server'; import { getTranslations } from 'next-intl/server'; import { notFound } from 'next/navigation'; @@ -9,7 +10,7 @@ interface UsersLayoutProps { export default async function UsersLayout({ children }: UsersLayoutProps) { // 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 const session = await getSession(); if (!session || (session.user.role !== 'admin' && !isDemo)) { diff --git a/src/app/[locale]/(protected)/settings/profile/page.tsx b/src/app/[locale]/(protected)/settings/profile/page.tsx index 3b14e97..6776077 100644 --- a/src/app/[locale]/(protected)/settings/profile/page.tsx +++ b/src/app/[locale]/(protected)/settings/profile/page.tsx @@ -1,20 +1,15 @@ import { UpdateAvatarCard } from '@/components/settings/profile/update-avatar-card'; import { UpdateNameCard } from '@/components/settings/profile/update-name-card'; -import { websiteConfig } from '@/config/website'; export default function ProfilePage() { - const enableUpdateAvatar = websiteConfig.features.enableUpdateAvatar; - return (
- {enableUpdateAvatar && ( -
- -
- )}
+
+ +
); } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 8e98301..4b17e75 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -14,8 +14,6 @@ type Href = Parameters[0]['href']; const staticRoutes = [ '/', '/pricing', - '/blog', - '/docs', '/about', '/contact', '/waitlist', @@ -25,6 +23,8 @@ const staticRoutes = [ '/cookie', '/auth/login', '/auth/register', + ...(websiteConfig.blog.enable ? ['/blog'] : []), + ...(websiteConfig.docs.enable ? ['/docs'] : []), ]; /** @@ -48,101 +48,106 @@ export default async function sitemap(): Promise { }) ); - // add categories - sitemapList.push( - ...categorySource.getPages().flatMap((category) => - routing.locales.map((locale) => ({ - url: getUrl(`/blog/category/${category.slugs[0]}`, locale), - 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) + // add blog related routes if enabled + if (websiteConfig.blog.enable) { + // add categories + sitemapList.push( + ...categorySource.getPages().flatMap((category) => + routing.locales.map((locale) => ({ + url: getUrl(`/blog/category/${category.slugs[0]}`, locale), + lastModified: new Date(), + priority: 0.8, + changeFrequency: 'weekly' as const, + })) + ) ); - // /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 - routing.locales.forEach((locale) => { - const localeCategories = categorySource.getPages(locale); - localeCategories.forEach((category) => { - // posts in this category and locale - const postsInCategory = blogSource + // add paginated blog list pages + routing.locales.forEach((locale) => { + const posts = blogSource .getPages(locale) - .filter((post) => post.data.published) - .filter((post) => - post.data.categories.some((cat) => cat === category.slugs[0]) - ); + .filter((post) => post.data.published); const totalPages = Math.max( 1, - Math.ceil(postsInCategory.length / websiteConfig.blog.paginationSize) + Math.ceil(posts.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) + // /blog/page/[page] (from 2) for (let page = 2; page <= totalPages; page++) { sitemapList.push({ - url: getUrl( - `/blog/category/${category.slugs[0]}/page/${page}`, - locale - ), + url: getUrl(`/blog/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), + // add paginated category pages + routing.locales.forEach((locale) => { + const localeCategories = categorySource.getPages(locale); + localeCategories.forEach((category) => { + // posts in this category and locale + const postsInCategory = blogSource + .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(), priority: 0.8, 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; } diff --git a/src/components/admin/user-detail-viewer.tsx b/src/components/admin/user-detail-viewer.tsx index 392ccf9..3936f58 100644 --- a/src/components/admin/user-detail-viewer.tsx +++ b/src/components/admin/user-detail-viewer.tsx @@ -23,6 +23,7 @@ import { Textarea } from '@/components/ui/textarea'; import { useIsMobile } from '@/hooks/use-mobile'; import { authClient } from '@/lib/auth-client'; import type { User } from '@/lib/auth-types'; +import { isDemoWebsite } from '@/lib/demo'; import { formatDate } from '@/lib/formatter'; import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls'; import { cn } from '@/lib/utils'; @@ -53,7 +54,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) { const triggerRefresh = useUsersStore((state) => state.triggerRefresh); // show fake data in demo website - const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; + const isDemo = isDemoWebsite(); const handleBan = async () => { if (!banReason) { diff --git a/src/components/admin/users-table.tsx b/src/components/admin/users-table.tsx index 7fd0bc3..f9f54f6 100644 --- a/src/components/admin/users-table.tsx +++ b/src/components/admin/users-table.tsx @@ -27,6 +27,7 @@ import { TableRow, } from '@/components/ui/table'; import type { User } from '@/lib/auth-types'; +import { isDemoWebsite } from '@/lib/demo'; import { formatDate } from '@/lib/formatter'; import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls'; import { IconCaretDownFilled, IconCaretUpFilled } from '@tabler/icons-react'; @@ -152,7 +153,7 @@ export function UsersTable({ const [columnVisibility, setColumnVisibility] = useState({}); // show fake data in demo website - const isDemo = process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; + const isDemo = isDemoWebsite(); // Map column IDs to translation keys const columnIdToTranslationKey = { diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx index cb7d855..5a07451 100644 --- a/src/components/dashboard/dashboard-header.tsx +++ b/src/components/dashboard/dashboard-header.tsx @@ -7,6 +7,7 @@ import { } from '@/components/ui/breadcrumb'; import { Separator } from '@/components/ui/separator'; import { SidebarTrigger } from '@/components/ui/sidebar'; +import { isDemoWebsite } from '@/lib/demo'; import React, { type ReactNode } from 'react'; import { CreditsBalanceButton } from '../layout/credits-balance-button'; import LocaleSwitcher from '../layout/locale-switcher'; @@ -30,8 +31,7 @@ export function DashboardHeader({ breadcrumbs, actions, }: DashboardHeaderProps) { - // 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 (
diff --git a/src/components/docs/youtube-video.tsx b/src/components/docs/youtube-video.tsx index 0a3aff1..4dfea6d 100644 --- a/src/components/docs/youtube-video.tsx +++ b/src/components/docs/youtube-video.tsx @@ -19,7 +19,7 @@ interface YoutubeVideoProps { export const YoutubeVideo = ({ url, width = 560, - height = 315, + height = 460, }: YoutubeVideoProps) => { return (
diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 12dc1ab..bf93f89 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -10,7 +10,6 @@ import { LocaleLink } from '@/i18n/navigation'; import { cn } from '@/lib/utils'; import { useTranslations } from 'next-intl'; import type React from 'react'; -import { ThemeSelector } from './theme-selector'; export function Footer({ className }: React.HTMLAttributes) { const t = useTranslations(); @@ -46,7 +45,7 @@ export function Footer({ className }: React.HTMLAttributes) { target="_blank" rel="noreferrer" 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" > {link.title} @@ -99,7 +98,6 @@ export function Footer({ className }: React.HTMLAttributes) {
- {/* */}
diff --git a/src/components/newsletter/newsletter-card.tsx b/src/components/newsletter/newsletter-card.tsx index 8b69129..8f00e88 100644 --- a/src/components/newsletter/newsletter-card.tsx +++ b/src/components/newsletter/newsletter-card.tsx @@ -1,10 +1,16 @@ 'use client'; import { NewsletterForm } from '@/components/newsletter/newsletter-form'; +import { websiteConfig } from '@/config/website'; import { useTranslations } from 'next-intl'; import { HeaderSection } from '../layout/header-section'; export function NewsletterCard() { + // show nothing if newsletter is disabled + if (!websiteConfig.newsletter.enable) { + return null; + } + const t = useTranslations('Newsletter'); return ( diff --git a/src/components/settings/notification/newsletter-form-card.tsx b/src/components/settings/notification/newsletter-form-card.tsx index 9aa13fa..6786c30 100644 --- a/src/components/settings/notification/newsletter-form-card.tsx +++ b/src/components/settings/notification/newsletter-form-card.tsx @@ -20,6 +20,7 @@ import { FormLabel, } from '@/components/ui/form'; import { Switch } from '@/components/ui/switch'; +import { websiteConfig } from '@/config/website'; import { authClient } from '@/lib/auth-client'; import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -40,6 +41,11 @@ interface NewsletterFormCardProps { * Allows users to toggle their newsletter subscription status */ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { + // show nothing if newsletter is disabled + if (!websiteConfig.newsletter.enable) { + return null; + } + const t = useTranslations('Dashboard.settings.notification'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); diff --git a/src/components/settings/profile/update-avatar-card.tsx b/src/components/settings/profile/update-avatar-card.tsx index 2da97f4..ff23b8d 100644 --- a/src/components/settings/profile/update-avatar-card.tsx +++ b/src/components/settings/profile/update-avatar-card.tsx @@ -11,6 +11,7 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; +import { websiteConfig } from '@/config/website'; import { authClient } from '@/lib/auth-client'; import { cn } from '@/lib/utils'; import { uploadFileFromBrowser } from '@/storage/client'; @@ -27,6 +28,14 @@ interface UpdateAvatarCardProps { * Update the user's avatar */ 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 [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); diff --git a/src/config/footer-config.tsx b/src/config/footer-config.tsx index d2fdbcf..a3207a7 100644 --- a/src/config/footer-config.tsx +++ b/src/config/footer-config.tsx @@ -3,6 +3,7 @@ import { Routes } from '@/routes'; import type { NestedMenuItem } from '@/types'; import { useTranslations } from 'next-intl'; +import { websiteConfig } from './website'; /** * Get footer config with translations @@ -41,16 +42,24 @@ export function getFooterLinks(): NestedMenuItem[] { { title: t('resources.title'), items: [ - { - title: t('resources.items.blog'), - href: Routes.Blog, - external: false, - }, - { - title: t('resources.items.docs'), - href: Routes.Docs, - external: false, - }, + ...(websiteConfig.blog.enable + ? [ + { + title: t('resources.items.blog'), + href: Routes.Blog, + external: false, + }, + ] + : []), + ...(websiteConfig.docs.enable + ? [ + { + title: t('resources.items.docs'), + href: Routes.Docs, + external: false, + }, + ] + : []), { title: t('resources.items.changelog'), href: Routes.Changelog, diff --git a/src/config/navbar-config.tsx b/src/config/navbar-config.tsx index 3bd9e85..ce14155 100644 --- a/src/config/navbar-config.tsx +++ b/src/config/navbar-config.tsx @@ -34,6 +34,7 @@ import { WandSparklesIcon, } from 'lucide-react'; import { useTranslations } from 'next-intl'; +import { websiteConfig } from './website'; /** * Get navbar config with translations @@ -59,16 +60,24 @@ export function getNavbarLinks(): NestedMenuItem[] { href: Routes.Pricing, external: false, }, - { - title: t('blog.title'), - href: Routes.Blog, - external: false, - }, - { - title: t('docs.title'), - href: Routes.Docs, - external: false, - }, + ...(websiteConfig.blog.enable + ? [ + { + title: t('blog.title'), + href: Routes.Blog, + external: false, + }, + ] + : []), + ...(websiteConfig.docs.enable + ? [ + { + title: t('docs.title'), + href: Routes.Docs, + external: false, + }, + ] + : []), { title: t('ai.title'), items: [ diff --git a/src/config/sidebar-config.tsx b/src/config/sidebar-config.tsx index 316aaa6..847a1e8 100644 --- a/src/config/sidebar-config.tsx +++ b/src/config/sidebar-config.tsx @@ -1,5 +1,6 @@ 'use client'; +import { isDemoWebsite } from '@/lib/demo'; import { Routes } from '@/routes'; import type { NestedMenuItem } from '@/types'; import { @@ -30,7 +31,7 @@ export function getSidebarLinks(): NestedMenuItem[] { const t = useTranslations('Dashboard'); // 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 [ { diff --git a/src/config/website.tsx b/src/config/website.tsx index c37715b..eda1eba 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -34,12 +34,12 @@ export const websiteConfig: WebsiteConfig = { }, features: { enableDiscordWidget: false, - enableCrispChat: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true', enableUpgradeCard: true, enableUpdateAvatar: true, enableAffonsoAffiliate: false, enablePromotekitAffiliate: false, enableDatafastRevenueTrack: false, + enableCrispChat: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true', enableTurnstileCaptcha: process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true', }, routes: { @@ -68,19 +68,25 @@ export const websiteConfig: WebsiteConfig = { }, }, blog: { + enable: true, paginationSize: 6, relatedPostsSize: 3, }, + docs: { + enable: true, + }, mail: { provider: 'resend', fromEmail: 'MkSaaS ', supportEmail: 'MkSaaS ', }, newsletter: { + enable: true, provider: 'resend', autoSubscribeAfterSignUp: true, }, storage: { + enable: true, provider: 's3', }, payment: { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 50ecedb..5cbfb47 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -164,7 +164,11 @@ export function getLocaleFromRequest(request?: Request): Locale { async function onCreateUser(user: User) { // 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 - 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 // This ensures the email verification email is sent first // 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 if ( + websiteConfig.credits.enableCredits && websiteConfig.credits.registerGiftCredits.enable && 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 - const pricePlans = await getAllPricePlans(); - const freePlan = pricePlans.find((plan) => plan.isFree); if ( - freePlan?.credits?.enable && - freePlan?.credits?.amount && - freePlan?.credits?.amount > 0 + websiteConfig.credits.enableCredits && + websiteConfig.credits.enableForFreePlan ) { - 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); + const pricePlans = await getAllPricePlans(); + const freePlan = pricePlans.find((plan) => plan.isFree); + if ( + freePlan?.credits?.enable && + freePlan?.credits?.amount && + freePlan?.credits?.amount > 0 + ) { + 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); + } } } } diff --git a/src/lib/demo.ts b/src/lib/demo.ts new file mode 100644 index 0000000..b28e6d8 --- /dev/null +++ b/src/lib/demo.ts @@ -0,0 +1,6 @@ +/** + * check if the website is a demo website + */ +export function isDemoWebsite() { + return process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true'; +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8fe2967..56fe117 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -13,6 +13,7 @@ export type WebsiteConfig = { auth: AuthConfig; i18n: I18nConfig; blog: BlogConfig; + docs: DocsConfig; mail: MailConfig; newsletter: NewsletterConfig; storage: StorageConfig; @@ -111,10 +112,18 @@ export interface I18nConfig { * Blog configuration */ export interface BlogConfig { + enable: boolean; // Whether to enable the blog paginationSize: number; // Number of posts per page relatedPostsSize: number; // Number of related posts to show } +/** + * Docs configuration + */ +export interface DocsConfig { + enable: boolean; // Whether to enable the docs +} + /** * Mail configuration */ @@ -128,6 +137,7 @@ export interface MailConfig { * Newsletter configuration */ export interface NewsletterConfig { + enable: boolean; // Whether to enable the newsletter provider: 'resend'; // The newsletter provider, only resend is supported for now autoSubscribeAfterSignUp?: boolean; // Whether to automatically subscribe users to the newsletter after sign up } @@ -136,6 +146,7 @@ export interface NewsletterConfig { * Storage configuration */ export interface StorageConfig { + enable: boolean; // Whether to enable the storage provider: 's3'; // The storage provider, only s3 is supported for now }