Merge remote-tracking branch 'origin/main' into cloudflare
This commit is contained in:
commit
63dd4e52fb
@ -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',
|
||||||
|
@ -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)) {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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)">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -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>('');
|
||||||
|
@ -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>('');
|
||||||
|
@ -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,
|
||||||
|
@ -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: [
|
||||||
|
@ -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 [
|
||||||
{
|
{
|
||||||
|
@ -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: {
|
||||||
|
@ -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
6
src/lib/demo.ts
Normal 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
11
src/types/index.d.ts
vendored
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user