- {table.getFilteredSelectedRowModel().rows.length} of{" "}
+ {table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
@@ -543,7 +538,7 @@ export function DataTable({
- Page {table.getState().pagination.pageIndex + 1} of{" "}
+ Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
@@ -624,34 +619,34 @@ export function DataTable({
- )
+ );
}
const chartData = [
- { month: "January", desktop: 186, mobile: 80 },
- { month: "February", desktop: 305, mobile: 200 },
- { month: "March", desktop: 237, mobile: 120 },
- { month: "April", desktop: 73, mobile: 190 },
- { month: "May", desktop: 209, mobile: 130 },
- { month: "June", desktop: 214, mobile: 140 },
-]
+ { month: 'January', desktop: 186, mobile: 80 },
+ { month: 'February', desktop: 305, mobile: 200 },
+ { month: 'March', desktop: 237, mobile: 120 },
+ { month: 'April', desktop: 73, mobile: 190 },
+ { month: 'May', desktop: 209, mobile: 130 },
+ { month: 'June', desktop: 214, mobile: 140 },
+];
const chartConfig = {
desktop: {
- label: "Desktop",
- color: "var(--primary)",
+ label: 'Desktop',
+ color: 'var(--primary)',
},
mobile: {
- label: "Mobile",
- color: "var(--primary)",
+ label: 'Mobile',
+ color: 'var(--primary)',
},
-} satisfies ChartConfig
+} satisfies ChartConfig;
function TableCellViewer({ item }: { item: z.infer
}) {
- const isMobile = useIsMobile()
+ const isMobile = useIsMobile();
return (
-
+
diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx
index 1c1a20d..e2b3161 100644
--- a/src/components/layout/footer.tsx
+++ b/src/components/layout/footer.tsx
@@ -1,15 +1,15 @@
'use client';
import Container from '@/components/layout/container';
-import { ModeSwitcherHorizontal } from '@/components/layout/mode-switcher-horizontal';
import { Logo } from '@/components/layout/logo';
+import { ModeSwitcherHorizontal } from '@/components/layout/mode-switcher-horizontal';
import BuiltWithButton from '@/components/shared/built-with-button';
import { getFooterLinks } from '@/config/footer-config';
import { getSocialLinks } from '@/config/social-config';
import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils';
import { useTranslations } from 'next-intl';
-import React from 'react';
+import type React from 'react';
import { ThemeSelector } from './theme-selector';
export function Footer({ className }: React.HTMLAttributes) {
diff --git a/src/components/layout/locale-selector.tsx b/src/components/layout/locale-selector.tsx
index ea5ca7c..a030a5a 100644
--- a/src/components/layout/locale-selector.tsx
+++ b/src/components/layout/locale-selector.tsx
@@ -11,7 +11,7 @@ import { websiteConfig } from '@/config/website';
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
import { DEFAULT_LOCALE } from '@/i18n/routing';
import { useLocaleStore } from '@/stores/locale-store';
-import { Locale, useLocale } from 'next-intl';
+import { type Locale, useLocale } from 'next-intl';
import { useParams } from 'next/navigation';
import { useEffect, useTransition } from 'react';
@@ -55,7 +55,7 @@ export default function LocaleSelector() {
{ locale: nextLocale }
);
});
- }
+ };
return (
@@ -77,7 +79,9 @@ export default function LocaleSelector() {
{currentLocale && (
{websiteConfig.i18n.locales[currentLocale].flag && (
- {websiteConfig.i18n.locales[currentLocale].flag}
+
+ {websiteConfig.i18n.locales[currentLocale].flag}
+
)}
{websiteConfig.i18n.locales[currentLocale].name}
@@ -92,9 +96,7 @@ export default function LocaleSelector() {
className="cursor-pointer flex items-center gap-2"
>
- {data.flag && (
- {data.flag}
- )}
+ {data.flag && {data.flag}}
{data.name}
diff --git a/src/components/layout/locale-switcher.tsx b/src/components/layout/locale-switcher.tsx
index bc79c0f..874b4ac 100644
--- a/src/components/layout/locale-switcher.tsx
+++ b/src/components/layout/locale-switcher.tsx
@@ -11,15 +11,15 @@ import { websiteConfig } from '@/config/website';
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
import { useLocaleStore } from '@/stores/locale-store';
import { Languages } from 'lucide-react';
-import { Locale, useLocale, useTranslations } from 'next-intl';
+import { type Locale, useLocale, useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { useEffect, useTransition } from 'react';
/**
* LocaleSwitcher component
- *
+ *
* Allows users to switch between available locales using a dropdown menu.
- *
+ *
* Based on next-intl's useLocaleRouter and useLocalePathname for locale navigation.
* https://next-intl.dev/docs/routing/navigation#userouter
*/
@@ -54,7 +54,7 @@ export default function LocaleSwitcher() {
{ locale: nextLocale }
);
});
- }
+ };
return (
@@ -69,18 +69,18 @@ export default function LocaleSwitcher() {
- {Object.entries(websiteConfig.i18n.locales).map(([localeOption, data]) => (
- setLocale(localeOption)}
- className="cursor-pointer"
- >
- {data.flag && (
- {data.flag}
- )}
- {data.name}
-
- ))}
+ {Object.entries(websiteConfig.i18n.locales).map(
+ ([localeOption, data]) => (
+ setLocale(localeOption)}
+ className="cursor-pointer"
+ >
+ {data.flag && {data.flag}}
+ {data.name}
+
+ )
+ )}
);
diff --git a/src/components/layout/logo.tsx b/src/components/layout/logo.tsx
index 153306f..d0f0be4 100644
--- a/src/components/layout/logo.tsx
+++ b/src/components/layout/logo.tsx
@@ -11,7 +11,7 @@ export function Logo({ className }: { className?: string }) {
const [mounted, setMounted] = useState(false);
const logoLight = websiteConfig.metadata.images?.logoLight ?? '/logo.png';
const logoDark = websiteConfig.metadata.images?.logoDark ?? logoLight;
-
+
// During server-side rendering and initial client render, always use logoLight
// This prevents hydration mismatch
const logo = mounted && theme === 'dark' ? logoDark : logoLight;
diff --git a/src/components/layout/navbar-mobile.tsx b/src/components/layout/navbar-mobile.tsx
index a25d142..287135f 100644
--- a/src/components/layout/navbar-mobile.tsx
+++ b/src/components/layout/navbar-mobile.tsx
@@ -20,7 +20,7 @@ import {
ChevronDownIcon,
ChevronRightIcon,
MenuIcon,
- XIcon
+ XIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import * as React from 'react';
@@ -76,9 +76,7 @@ export function NavbarMobile({
{/* navbar left shows logo */}
-
- {t('Metadata.name')}
-
+ {t('Metadata.name')}
{/* navbar right shows menu icon and user button */}
@@ -86,9 +84,9 @@ export function NavbarMobile({
{/* show user button if user is logged in */}
{isPending ? (
- ) : (
- currentUser ?
: null
- )}
+ ) : currentUser ? (
+
+ ) : null}
- subItem.href && localePathname.startsWith(subItem.href)
- );
+ (subItem) =>
+ subItem.href && localePathname.startsWith(subItem.href)
+ );
return (
@@ -210,7 +208,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground',
isActive &&
- 'font-semibold bg-transparent text-foreground'
+ 'font-semibold bg-transparent text-foreground'
)}
>
{item.title}
@@ -247,7 +245,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground',
isSubItemActive &&
- 'font-semibold bg-transparent text-foreground'
+ 'font-semibold bg-transparent text-foreground'
)}
onClick={onLinkClicked}
>
@@ -258,7 +256,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
- 'bg-transparent text-foreground'
+ 'bg-transparent text-foreground'
)}
>
{subItem.icon ? subItem.icon : null}
@@ -270,7 +268,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
- 'font-semibold bg-transparent text-foreground'
+ 'font-semibold bg-transparent text-foreground'
)}
>
{subItem.title}
@@ -297,7 +295,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
- 'bg-transparent text-foreground'
+ 'bg-transparent text-foreground'
)}
/>
)}
@@ -319,7 +317,8 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
'bg-transparent text-muted-foreground',
'hover:bg-transparent hover:text-foreground',
'focus:bg-transparent focus:text-foreground',
- isActive && 'font-semibold bg-transparent text-foreground'
+ isActive &&
+ 'font-semibold bg-transparent text-foreground'
)}
onClick={onLinkClicked}
>
diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx
index 78682b1..a504737 100644
--- a/src/components/layout/navbar.tsx
+++ b/src/components/layout/navbar.tsx
@@ -124,7 +124,7 @@ export function Navbar({ scroll }: NavBarProps) {
'hover:bg-accent hover:text-accent-foreground',
'focus:bg-accent focus:text-accent-foreground',
isSubItemActive &&
- 'bg-accent text-accent-foreground'
+ 'bg-accent text-accent-foreground'
)}
>
{subItem.icon ? subItem.icon : null}
@@ -146,7 +146,7 @@ export function Navbar({ scroll }: NavBarProps) {
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
- 'bg-transparent text-foreground'
+ 'bg-transparent text-foreground'
)}
>
{subItem.title}
@@ -158,7 +158,7 @@ export function Navbar({ scroll }: NavBarProps) {
'group-hover:bg-transparent group-hover:text-foreground/80',
'group-focus:bg-transparent group-focus:text-foreground/80',
isSubItemActive &&
- 'bg-transparent text-foreground/80'
+ 'bg-transparent text-foreground/80'
)}
>
{subItem.description}
@@ -172,7 +172,7 @@ export function Navbar({ scroll }: NavBarProps) {
'group-hover:bg-transparent group-hover:text-foreground',
'group-focus:bg-transparent group-focus:text-foreground',
isSubItemActive &&
- 'bg-transparent text-foreground'
+ 'bg-transparent text-foreground'
)}
/>
)}
@@ -216,24 +216,26 @@ export function Navbar({ scroll }: NavBarProps) {
{isPending ? (
+ ) : currentUser ? (
+
) : (
- currentUser ? (
-
- ) : (
-
-
-
- {t('Common.login')}
-
-
-
-
-
- {t('Common.signUp')}
-
+
+
+
+ {t('Common.login')}
-
- )
+
+
+
+
+ {t('Common.signUp')}
+
+
+
)}
diff --git a/src/components/layout/payment-provider.tsx b/src/components/layout/payment-provider.tsx
index 895a2f6..a7ae915 100644
--- a/src/components/layout/payment-provider.tsx
+++ b/src/components/layout/payment-provider.tsx
@@ -1,12 +1,12 @@
'use client';
-import { usePaymentStore } from '@/stores/payment-store';
import { authClient } from '@/lib/auth-client';
+import { usePaymentStore } from '@/stores/payment-store';
import { useEffect } from 'react';
/**
* Payment provider component
- *
+ *
* This component is responsible for initializing the payment state
* by fetching the current user's subscription and payment information when the app loads.
*/
@@ -21,4 +21,4 @@ export function PaymentProvider({ children }: { children: React.ReactNode }) {
}, [session, fetchPayment]);
return <>{children}>;
-}
\ No newline at end of file
+}
diff --git a/src/components/layout/theme-selector.tsx b/src/components/layout/theme-selector.tsx
index 3a7ed7c..ba8af28 100644
--- a/src/components/layout/theme-selector.tsx
+++ b/src/components/layout/theme-selector.tsx
@@ -1,17 +1,17 @@
-"use client";
+'use client';
-import { Label } from "@/components/ui/label";
+import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
- SelectValue
-} from "@/components/ui/select";
-import { websiteConfig } from "@/config/website";
-import { useTranslations } from "next-intl";
-import { useThemeConfig } from "./active-theme-provider";
+ SelectValue,
+} from '@/components/ui/select';
+import { websiteConfig } from '@/config/website';
+import { useTranslations } from 'next-intl';
+import { useThemeConfig } from './active-theme-provider';
/**
* 1. The component allows the user to select the theme of the website
@@ -31,23 +31,23 @@ export function ThemeSelector() {
const DEFAULT_THEMES = [
{
name: t('default'),
- value: "default",
+ value: 'default',
},
{
name: t('neutral'),
- value: "neutral",
+ value: 'neutral',
},
{
name: t('blue'),
- value: "blue",
+ value: 'blue',
},
{
name: t('green'),
- value: "green",
+ value: 'green',
},
{
name: t('amber'),
- value: "amber",
+ value: 'amber',
},
];
@@ -67,7 +67,9 @@ export function ThemeSelector() {
{DEFAULT_THEMES.map((theme) => (
-
{theme.name}
diff --git a/src/components/layout/user-avatar.tsx b/src/components/layout/user-avatar.tsx
index c8fd746..1a6418d 100644
--- a/src/components/layout/user-avatar.tsx
+++ b/src/components/layout/user-avatar.tsx
@@ -9,7 +9,7 @@ interface UserAvatarProps extends AvatarProps {
/**
* User avatar component, used in navbar and sidebar
- *
+ *
* @param name - The name of the user
* @param image - The image of the user
* @param props - The props of the avatar
@@ -18,11 +18,7 @@ interface UserAvatarProps extends AvatarProps {
export function UserAvatar({ name, image, ...props }: UserAvatarProps) {
return (
-
+
{name}
diff --git a/src/components/layout/user-button-mobile.tsx b/src/components/layout/user-button-mobile.tsx
index 3643cb9..9924972 100644
--- a/src/components/layout/user-button-mobile.tsx
+++ b/src/components/layout/user-button-mobile.tsx
@@ -14,7 +14,7 @@ import { getAvatarLinks } from '@/config/avatar-config';
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { usePaymentStore } from '@/stores/payment-store';
-import { User } from 'better-auth';
+import type { User } from 'better-auth';
import { LogOutIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
@@ -77,9 +77,7 @@ export function UserButtonMobile({ user }: UserButtonProps) {
className="size-8 border cursor-pointer"
/>
-
- {user.name}
-
+
{user.name}
{user.email}
diff --git a/src/components/layout/user-button.tsx b/src/components/layout/user-button.tsx
index 78af2d0..cf8ba4f 100644
--- a/src/components/layout/user-button.tsx
+++ b/src/components/layout/user-button.tsx
@@ -12,7 +12,7 @@ import { getAvatarLinks } from '@/config/avatar-config';
import { useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { usePaymentStore } from '@/stores/payment-store';
-import { User } from 'better-auth';
+import type { User } from 'better-auth';
import { LogOutIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
@@ -44,7 +44,7 @@ export function UserButton({ user }: UserButtonProps) {
},
},
});
- }
+ };
// Desktop View, use DropdownMenu
return (
@@ -59,9 +59,7 @@ export function UserButton({ user }: UserButtonProps) {
-
- {user.name}
-
+
{user.name}
{user.email}
diff --git a/src/components/motion/animated-group.tsx b/src/components/motion/animated-group.tsx
index 0934dd1..963b43a 100644
--- a/src/components/motion/animated-group.tsx
+++ b/src/components/motion/animated-group.tsx
@@ -1,122 +1,138 @@
-'use client'
-import { ReactNode } from 'react'
-import { motion, Variants } from 'motion/react'
-import React from 'react'
+'use client';
+import { type Variants, motion } from 'motion/react';
+import type { ReactNode } from 'react';
+import React from 'react';
-export type PresetType = 'fade' | 'slide' | 'scale' | 'blur' | 'blur-slide' | 'zoom' | 'flip' | 'bounce' | 'rotate' | 'swing'
+export type PresetType =
+ | 'fade'
+ | 'slide'
+ | 'scale'
+ | 'blur'
+ | 'blur-slide'
+ | 'zoom'
+ | 'flip'
+ | 'bounce'
+ | 'rotate'
+ | 'swing';
export type AnimatedGroupProps = {
- children: ReactNode
- className?: string
- variants?: {
- container?: Variants
- item?: Variants
- }
- preset?: PresetType
- as?: React.ElementType
- asChild?: React.ElementType
-}
+ children: ReactNode;
+ className?: string;
+ variants?: {
+ container?: Variants;
+ item?: Variants;
+ };
+ preset?: PresetType;
+ as?: React.ElementType;
+ asChild?: React.ElementType;
+};
const defaultContainerVariants: Variants = {
- visible: {
- transition: {
- staggerChildren: 0.1,
- },
+ visible: {
+ transition: {
+ staggerChildren: 0.1,
},
-}
+ },
+};
const defaultItemVariants: Variants = {
- hidden: { opacity: 0 },
- visible: { opacity: 1 },
-}
+ hidden: { opacity: 0 },
+ visible: { opacity: 1 },
+};
const presetVariants: Record
= {
- fade: {},
- slide: {
- hidden: { y: 20 },
- visible: { y: 0 },
+ fade: {},
+ slide: {
+ hidden: { y: 20 },
+ visible: { y: 0 },
+ },
+ scale: {
+ hidden: { scale: 0.8 },
+ visible: { scale: 1 },
+ },
+ blur: {
+ hidden: { filter: 'blur(4px)' },
+ visible: { filter: 'blur(0px)' },
+ },
+ 'blur-slide': {
+ hidden: { filter: 'blur(4px)', y: 20 },
+ visible: { filter: 'blur(0px)', y: 0 },
+ },
+ zoom: {
+ hidden: { scale: 0.5 },
+ visible: {
+ scale: 1,
+ transition: { type: 'spring', stiffness: 300, damping: 20 },
},
- scale: {
- hidden: { scale: 0.8 },
- visible: { scale: 1 },
+ },
+ flip: {
+ hidden: { rotateX: -90 },
+ visible: {
+ rotateX: 0,
+ transition: { type: 'spring', stiffness: 300, damping: 20 },
},
- blur: {
- hidden: { filter: 'blur(4px)' },
- visible: { filter: 'blur(0px)' },
+ },
+ bounce: {
+ hidden: { y: -50 },
+ visible: {
+ y: 0,
+ transition: { type: 'spring', stiffness: 400, damping: 10 },
},
- 'blur-slide': {
- hidden: { filter: 'blur(4px)', y: 20 },
- visible: { filter: 'blur(0px)', y: 0 },
+ },
+ rotate: {
+ hidden: { rotate: -180 },
+ visible: {
+ rotate: 0,
+ transition: { type: 'spring', stiffness: 200, damping: 15 },
},
- zoom: {
- hidden: { scale: 0.5 },
- visible: {
- scale: 1,
- transition: { type: 'spring', stiffness: 300, damping: 20 },
- },
+ },
+ swing: {
+ hidden: { rotate: -10 },
+ visible: {
+ rotate: 0,
+ transition: { type: 'spring', stiffness: 300, damping: 8 },
},
- flip: {
- hidden: { rotateX: -90 },
- visible: {
- rotateX: 0,
- transition: { type: 'spring', stiffness: 300, damping: 20 },
- },
- },
- bounce: {
- hidden: { y: -50 },
- visible: {
- y: 0,
- transition: { type: 'spring', stiffness: 400, damping: 10 },
- },
- },
- rotate: {
- hidden: { rotate: -180 },
- visible: {
- rotate: 0,
- transition: { type: 'spring', stiffness: 200, damping: 15 },
- },
- },
- swing: {
- hidden: { rotate: -10 },
- visible: {
- rotate: 0,
- transition: { type: 'spring', stiffness: 300, damping: 8 },
- },
- },
-}
+ },
+};
const addDefaultVariants = (variants: Variants) => ({
- hidden: { ...defaultItemVariants.hidden, ...variants.hidden },
- visible: { ...defaultItemVariants.visible, ...variants.visible },
-})
+ hidden: { ...defaultItemVariants.hidden, ...variants.hidden },
+ visible: { ...defaultItemVariants.visible, ...variants.visible },
+});
-function AnimatedGroup({ children, className, variants, preset, as = 'div', asChild = 'div' }: AnimatedGroupProps) {
- const selectedVariants = {
- item: addDefaultVariants(preset ? presetVariants[preset] : {}),
- container: addDefaultVariants(defaultContainerVariants),
- }
- const containerVariants = variants?.container || selectedVariants.container
- const itemVariants = variants?.item || selectedVariants.item
+function AnimatedGroup({
+ children,
+ className,
+ variants,
+ preset,
+ as = 'div',
+ asChild = 'div',
+}: AnimatedGroupProps) {
+ const selectedVariants = {
+ item: addDefaultVariants(preset ? presetVariants[preset] : {}),
+ container: addDefaultVariants(defaultContainerVariants),
+ };
+ const containerVariants = variants?.container || selectedVariants.container;
+ const itemVariants = variants?.item || selectedVariants.item;
- const MotionComponent = motion(as)
+ const MotionComponent = motion(as);
- const MotionChild = motion(asChild)
+ const MotionChild = motion(asChild);
- return (
-
- {React.Children.map(children, (child, index) => (
-
- {child}
-
- ))}
-
- )
+ return (
+
+ {React.Children.map(children, (child, index) => (
+
+ {child}
+
+ ))}
+
+ );
}
-export { AnimatedGroup }
+export { AnimatedGroup };
diff --git a/src/components/motion/infinite-slider.tsx b/src/components/motion/infinite-slider.tsx
index 87d851e..8b8e9fc 100644
--- a/src/components/motion/infinite-slider.tsx
+++ b/src/components/motion/infinite-slider.tsx
@@ -1,7 +1,7 @@
'use client';
import { cn } from '@/lib/utils';
-import { useMotionValue, animate, motion } from 'motion/react';
-import { useState, useEffect } from 'react';
+import { animate, motion, useMotionValue } from 'motion/react';
+import { useEffect, useState } from 'react';
import useMeasure from 'react-use-measure';
export type InfiniteSliderProps = {
@@ -55,7 +55,7 @@ export function InfiniteSlider({
controls = animate(translation, [from, to], {
ease: 'linear',
duration: duration,
- repeat: Infinity,
+ repeat: Number.POSITIVE_INFINITY,
repeatType: 'loop',
repeatDelay: 0,
onRepeat: () => {
@@ -93,7 +93,7 @@ export function InfiniteSlider({
return (
void
- onAnimationStart?: () => void
- segmentWrapperClassName?: string
- containerTransition?: Transition
- segmentTransition?: Transition
- style?: React.CSSProperties
-}
+ container?: Variants;
+ item?: Variants;
+ };
+ className?: string;
+ preset?: PresetType;
+ delay?: number;
+ speedReveal?: number;
+ speedSegment?: number;
+ trigger?: boolean;
+ onAnimationComplete?: () => void;
+ onAnimationStart?: () => void;
+ segmentWrapperClassName?: string;
+ containerTransition?: Transition;
+ segmentTransition?: Transition;
+ style?: React.CSSProperties;
+};
const defaultStaggerTimes: Record = {
char: 0.03,
word: 0.05,
line: 0.1,
-}
+};
const defaultContainerVariants: Variants = {
hidden: { opacity: 0 },
@@ -46,7 +53,7 @@ const defaultContainerVariants: Variants = {
exit: {
transition: { staggerChildren: 0.05, staggerDirection: -1 },
},
-}
+};
const defaultItemVariants: Variants = {
hidden: { opacity: 0 },
@@ -54,9 +61,12 @@ const defaultItemVariants: Variants = {
opacity: 1,
},
exit: { opacity: 0 },
-}
+};
-const presetVariants: Record = {
+const presetVariants: Record<
+ PresetType,
+ { container: Variants; item: Variants }
+> = {
blur: {
container: defaultContainerVariants,
item: {
@@ -97,26 +107,25 @@ const presetVariants: Record = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {
const content =
per === 'line' ? (
-
+
{segment}
) : per === 'word' ? (
+ className="inline-block whitespace-pre"
+ >
{segment}
) : (
@@ -126,87 +135,131 @@ const AnimationComponent: React.FC<{
key={`char-${charIndex}`}
aria-hidden="true"
variants={variants}
- className="inline-block whitespace-pre">
+ className="inline-block whitespace-pre"
+ >
{char}
))}
- )
+ );
if (!segmentWrapperClassName) {
- return content
+ return content;
}
- const defaultWrapperClassName = per === 'line' ? 'block' : 'inline-block'
+ const defaultWrapperClassName = per === 'line' ? 'block' : 'inline-block';
- return {content}
-})
+ return (
+
+ {content}
+
+ );
+});
-AnimationComponent.displayName = 'AnimationComponent'
+AnimationComponent.displayName = 'AnimationComponent';
const splitText = (text: string, per: 'line' | 'word' | 'char') => {
- if (per === 'line') return text.split('\n')
- return text.split(/(\s+)/)
-}
+ if (per === 'line') return text.split('\n');
+ return text.split(/(\s+)/);
+};
-const hasTransition = (variant: Variant): variant is TargetAndTransition & { transition?: Transition } => {
- return typeof variant === 'object' && variant !== null && 'transition' in variant
-}
+const hasTransition = (
+ variant: Variant
+): variant is TargetAndTransition & { transition?: Transition } => {
+ return (
+ typeof variant === 'object' && variant !== null && 'transition' in variant
+ );
+};
-const createVariantsWithTransition = (baseVariants: Variants, transition?: Transition & { exit?: Transition }): Variants => {
- if (!transition) return baseVariants
+const createVariantsWithTransition = (
+ baseVariants: Variants,
+ transition?: Transition & { exit?: Transition }
+): Variants => {
+ if (!transition) return baseVariants;
- const { ...mainTransition } = transition
+ const { ...mainTransition } = transition;
return {
...baseVariants,
visible: {
...baseVariants.visible,
transition: {
- ...(hasTransition(baseVariants.visible) ? baseVariants.visible.transition : {}),
+ ...(hasTransition(baseVariants.visible)
+ ? baseVariants.visible.transition
+ : {}),
...mainTransition,
},
},
exit: {
...baseVariants.exit,
transition: {
- ...(hasTransition(baseVariants.exit) ? baseVariants.exit.transition : {}),
+ ...(hasTransition(baseVariants.exit)
+ ? baseVariants.exit.transition
+ : {}),
...mainTransition,
staggerDirection: -1,
},
},
- }
-}
+ };
+};
-export function TextEffect({ children, per = 'word', as = 'p', variants, className, preset = 'fade', delay = 0, speedReveal = 1, speedSegment = 1, trigger = true, onAnimationComplete, onAnimationStart, segmentWrapperClassName, containerTransition, segmentTransition, style }: TextEffectProps) {
- const segments = splitText(children, per)
- const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
+export function TextEffect({
+ children,
+ per = 'word',
+ as = 'p',
+ variants,
+ className,
+ preset = 'fade',
+ delay = 0,
+ speedReveal = 1,
+ speedSegment = 1,
+ trigger = true,
+ onAnimationComplete,
+ onAnimationStart,
+ segmentWrapperClassName,
+ containerTransition,
+ segmentTransition,
+ style,
+}: TextEffectProps) {
+ const segments = splitText(children, per);
+ const MotionTag = motion[as as keyof typeof motion] as typeof motion.div;
- const baseVariants = preset ? presetVariants[preset] : { container: defaultContainerVariants, item: defaultItemVariants }
+ const baseVariants = preset
+ ? presetVariants[preset]
+ : { container: defaultContainerVariants, item: defaultItemVariants };
- const stagger = defaultStaggerTimes[per] / speedReveal
+ const stagger = defaultStaggerTimes[per] / speedReveal;
- const baseDuration = 0.3 / speedSegment
+ const baseDuration = 0.3 / speedSegment;
- const customStagger = hasTransition(variants?.container?.visible ?? {}) ? (variants?.container?.visible as TargetAndTransition).transition?.staggerChildren : undefined
+ const customStagger = hasTransition(variants?.container?.visible ?? {})
+ ? (variants?.container?.visible as TargetAndTransition).transition
+ ?.staggerChildren
+ : undefined;
- const customDelay = hasTransition(variants?.container?.visible ?? {}) ? (variants?.container?.visible as TargetAndTransition).transition?.delayChildren : undefined
+ const customDelay = hasTransition(variants?.container?.visible ?? {})
+ ? (variants?.container?.visible as TargetAndTransition).transition
+ ?.delayChildren
+ : undefined;
const computedVariants = {
- container: createVariantsWithTransition(variants?.container || baseVariants.container, {
- staggerChildren: customStagger ?? stagger,
- delayChildren: customDelay ?? delay,
- ...containerTransition,
- exit: {
+ container: createVariantsWithTransition(
+ variants?.container || baseVariants.container,
+ {
staggerChildren: customStagger ?? stagger,
- staggerDirection: -1,
- },
- }),
+ delayChildren: customDelay ?? delay,
+ ...containerTransition,
+ exit: {
+ staggerChildren: customStagger ?? stagger,
+ staggerDirection: -1,
+ },
+ }
+ ),
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
duration: baseDuration,
...segmentTransition,
}),
- }
+ };
return (
@@ -219,7 +272,8 @@ export function TextEffect({ children, per = 'word', as = 'p', variants, classNa
className={className}
onAnimationComplete={onAnimationComplete}
onAnimationStart={onAnimationStart}
- style={style}>
+ style={style}
+ >
{per !== 'line' ? {children} : null}
{segments.map((segment, index) => (
)}
- )
+ );
}
diff --git a/src/components/nsui/blocks.ts b/src/components/nsui/blocks.ts
index 6537601..173a676 100644
--- a/src/components/nsui/blocks.ts
+++ b/src/components/nsui/blocks.ts
@@ -379,7 +379,6 @@ export const blocks: Block[] = [
// code: loadCode('app/preview/testimonials/six/page.tsx'),
},
-
{
slug: 'call-to-action',
title: 'one',
@@ -581,7 +580,7 @@ export const blocks: Block[] = [
category: 'contact',
preview: '/preview/contact/two',
// code: loadCode('app/preview/contact/two/page.tsx'),
- }
+ },
];
export const categories = [...new Set(blocks.map((b) => b.category))];
diff --git a/src/components/page/custom-page.tsx b/src/components/page/custom-page.tsx
index 4d56093..0a60af3 100644
--- a/src/components/page/custom-page.tsx
+++ b/src/components/page/custom-page.tsx
@@ -38,7 +38,10 @@ export function CustomPage({
-
+
diff --git a/src/components/pricing/create-checkout-button.tsx b/src/components/pricing/create-checkout-button.tsx
index fd3f139..c68d9ba 100644
--- a/src/components/pricing/create-checkout-button.tsx
+++ b/src/components/pricing/create-checkout-button.tsx
@@ -12,7 +12,14 @@ interface CheckoutButtonProps {
planId: string;
priceId: string;
metadata?: Record;
- variant?: 'default' | 'outline' | 'destructive' | 'secondary' | 'ghost' | 'link' | null;
+ variant?:
+ | 'default'
+ | 'outline'
+ | 'destructive'
+ | 'secondary'
+ | 'ghost'
+ | 'link'
+ | null;
size?: 'default' | 'sm' | 'lg' | 'icon' | null;
className?: string;
children?: React.ReactNode;
@@ -20,10 +27,10 @@ interface CheckoutButtonProps {
/**
* Checkout Button
- *
+ *
* This client component creates a Stripe checkout session and redirects to it
* It's used to initiate the checkout process for a specific plan and price.
- *
+ *
* NOTICE: Login is required when using this button.
*/
export function CheckoutButton({
@@ -84,4 +91,4 @@ export function CheckoutButton({
)}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/pricing/customer-portal-button.tsx b/src/components/pricing/customer-portal-button.tsx
index 7ae9337..846b733 100644
--- a/src/components/pricing/customer-portal-button.tsx
+++ b/src/components/pricing/customer-portal-button.tsx
@@ -11,7 +11,14 @@ import { toast } from 'sonner';
interface CustomerPortalButtonProps {
userId: string;
returnUrl?: string;
- variant?: 'default' | 'outline' | 'destructive' | 'secondary' | 'ghost' | 'link' | null;
+ variant?:
+ | 'default'
+ | 'outline'
+ | 'destructive'
+ | 'secondary'
+ | 'ghost'
+ | 'link'
+ | null;
size?: 'default' | 'sm' | 'lg' | 'icon' | null;
className?: string;
children?: React.ReactNode;
@@ -19,10 +26,10 @@ interface CustomerPortalButtonProps {
/**
* Customer Portal Button
- *
+ *
* This client component opens the Stripe customer portal
* It's used to let customers manage their billing, subscriptions, and payment methods
- *
+ *
* NOTICE: Login is required when using this button.
*/
export function CustomerPortalButton({
@@ -79,4 +86,4 @@ export function CustomerPortalButton({
)}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/pricing/pricing-card.tsx b/src/components/pricing/pricing-card.tsx
index bf49ac5..c2b729a 100644
--- a/src/components/pricing/pricing-card.tsx
+++ b/src/components/pricing/pricing-card.tsx
@@ -1,12 +1,25 @@
'use client';
import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
import { useCurrentUser } from '@/hooks/use-current-user';
import { useLocalePathname } from '@/i18n/navigation';
import { formatPrice } from '@/lib/formatter';
import { cn } from '@/lib/utils';
-import { PaymentType, PaymentTypes, PlanInterval, PlanIntervals, Price, PricePlan } from '@/payment/types';
+import {
+ type PaymentType,
+ PaymentTypes,
+ type PlanInterval,
+ PlanIntervals,
+ type Price,
+ type PricePlan,
+} from '@/payment/types';
import { CheckCircleIcon, XCircleIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { LoginWrapper } from '../auth/login-wrapper';
@@ -33,22 +46,25 @@ function getPriceForPlan(
interval?: PlanInterval,
paymentType?: PaymentType
): Price | undefined {
- if (plan.isFree) { // Free plan has no price
+ if (plan.isFree) {
+ // Free plan has no price
return undefined;
}
// non-free plans must have a price
- return plan.prices.find(price => {
+ return plan.prices.find((price) => {
if (paymentType === PaymentTypes.ONE_TIME) {
return price.type === PaymentTypes.ONE_TIME;
}
- return price.type === PaymentTypes.SUBSCRIPTION && price.interval === interval;
+ return (
+ price.type === PaymentTypes.SUBSCRIPTION && price.interval === interval
+ );
});
}
/**
* Pricing Card Component
- *
+ *
* Displays a single pricing plan with features and action button
*/
export function PricingCard({
@@ -70,7 +86,8 @@ export function PricingCard({
let priceLabel = '';
if (plan.isFree) {
formattedPrice = t('freePrice');
- } else if (price && price.amount > 0) { // price is available
+ } else if (price && price.amount > 0) {
+ // price is available
formattedPrice = formatPrice(price.amount, price.currency);
if (interval === PlanIntervals.MONTH) {
priceLabel = t('perMonth');
@@ -89,24 +106,29 @@ export function PricingCard({
return (
{/* show popular badge if plan is recommended */}
{plan.recommended && (
-
+
{t('popular')}
)}
{/* show current plan badge if plan is current plan */}
{isCurrentPlan && (
-
+
{t('currentPlan')}
)}
@@ -121,10 +143,7 @@ export function PricingCard({
{formattedPrice}
- {priceLabel &&
-
- {priceLabel}
- }
+ {priceLabel && {priceLabel}}
@@ -145,8 +164,11 @@ export function PricingCard({
)
) : isCurrentPlan ? (
-
+
{t('yourCurrentPlan')}
) : isPaidPlan ? (
@@ -180,8 +202,10 @@ export function PricingCard({
{/* show trial period if it exists */}
{hasTrialPeriod && (
-
+
{t('daysTrial', { days: price.trialPeriodDays as number })}
@@ -209,4 +233,4 @@ export function PricingCard({
);
-}
\ No newline at end of file
+}
diff --git a/src/components/pricing/pricing-table.tsx b/src/components/pricing/pricing-table.tsx
index 1f33324..0c1bf39 100644
--- a/src/components/pricing/pricing-table.tsx
+++ b/src/components/pricing/pricing-table.tsx
@@ -3,7 +3,12 @@
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { getPricePlans } from '@/config/payment-config';
import { cn } from '@/lib/utils';
-import { PaymentTypes, PlanInterval, PlanIntervals, PricePlan } from '@/payment/types';
+import {
+ PaymentTypes,
+ type PlanInterval,
+ PlanIntervals,
+ type PricePlan,
+} from '@/payment/types';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { PricingCard } from './pricing-card';
@@ -16,7 +21,7 @@ interface PricingTableProps {
/**
* Pricing Table Component
- *
+ *
* 1. Displays all pricing plans with interval selection tabs for subscription plans,
* free plans and one-time purchase plans are always displayed
* 2. If a plan is disabled, it will not be displayed in the pricing table
@@ -38,28 +43,42 @@ export function PricingTable({
const currentPlanId = currentPlan?.id || null;
// Filter plans into free, subscription and one-time plans
- const freePlans = plans.filter(plan => plan.isFree && !plan.disabled);
+ const freePlans = plans.filter((plan) => plan.isFree && !plan.disabled);
- const subscriptionPlans = plans.filter(plan =>
- !plan.isFree && !plan.disabled
- && plan.prices.some(price => !price.disabled && price.type === PaymentTypes.SUBSCRIPTION)
+ const subscriptionPlans = plans.filter(
+ (plan) =>
+ !plan.isFree &&
+ !plan.disabled &&
+ plan.prices.some(
+ (price) => !price.disabled && price.type === PaymentTypes.SUBSCRIPTION
+ )
);
- const oneTimePlans = plans.filter(plan =>
- !plan.isFree && !plan.disabled
- && plan.prices.some(price => !price.disabled && price.type === PaymentTypes.ONE_TIME)
+ const oneTimePlans = plans.filter(
+ (plan) =>
+ !plan.isFree &&
+ !plan.disabled &&
+ plan.prices.some(
+ (price) => !price.disabled && price.type === PaymentTypes.ONE_TIME
+ )
);
// Check if any plan has a monthly price option
- const hasMonthlyOption = subscriptionPlans.some(plan =>
- plan.prices.some(price => price.type === PaymentTypes.SUBSCRIPTION
- && price.interval === PlanIntervals.MONTH)
+ const hasMonthlyOption = subscriptionPlans.some((plan) =>
+ plan.prices.some(
+ (price) =>
+ price.type === PaymentTypes.SUBSCRIPTION &&
+ price.interval === PlanIntervals.MONTH
+ )
);
// Check if any plan has a yearly price option
- const hasYearlyOption = subscriptionPlans.some(plan =>
- plan.prices.some(price => price.type === PaymentTypes.SUBSCRIPTION
- && price.interval === PlanIntervals.YEAR)
+ const hasYearlyOption = subscriptionPlans.some((plan) =>
+ plan.prices.some(
+ (price) =>
+ price.type === PaymentTypes.SUBSCRIPTION &&
+ price.interval === PlanIntervals.YEAR
+ )
);
const handleIntervalChange = (value: string) => {
@@ -67,44 +86,60 @@ export function PricingTable({
};
return (
-
+
{/* Show interval toggle if there are subscription plans */}
- {(hasMonthlyOption || hasYearlyOption) && subscriptionPlans.length > 0 && (
-
- value && handleIntervalChange(value)}
- className="border rounded-lg p-1"
- >
- {hasMonthlyOption && (
-
- {t('monthly')}
-
- )}
- {hasYearlyOption && (
-
- {t('yearly')}
-
- )}
-
-
- )}
+ {(hasMonthlyOption || hasYearlyOption) &&
+ subscriptionPlans.length > 0 && (
+
+ value && handleIntervalChange(value)}
+ className="border rounded-lg p-1"
+ >
+ {hasMonthlyOption && (
+
+ {t('monthly')}
+
+ )}
+ {hasYearlyOption && (
+
+ {t('yearly')}
+
+ )}
+
+
+ )}
{/* Calculate total number of visible plans */}
{(() => {
- const totalVisiblePlans = freePlans.length + subscriptionPlans.length + oneTimePlans.length;
+ const totalVisiblePlans =
+ freePlans.length + subscriptionPlans.length + oneTimePlans.length;
return (
-
= 3 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
- )}>
+
= 3 &&
+ 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
+ )}
+ >
{/* Render free plans (always visible) */}
{freePlans.map((plan) => (
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/billing/billing-card.tsx b/src/components/settings/billing/billing-card.tsx
index 8745a5c..489919e 100644
--- a/src/components/settings/billing/billing-card.tsx
+++ b/src/components/settings/billing/billing-card.tsx
@@ -3,7 +3,14 @@
import { CustomerPortalButton } from '@/components/pricing/customer-portal-button';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { getPricePlans } from '@/config/payment-config';
import { usePayment } from '@/hooks/use-payment';
@@ -24,26 +31,31 @@ export default function BillingCard() {
error: loadPaymentError,
subscription,
currentPlan: currentPlanFromStore,
- refetch
+ refetch,
} = usePayment();
// Get user session for customer ID
- const { data: session, isPending: isLoadingSession } = authClient.useSession();
+ const { data: session, isPending: isLoadingSession } =
+ authClient.useSession();
const currentUser = session?.user;
// Get price plans with translations - must be called here to maintain hook order
const pricePlans = getPricePlans();
const plans = Object.values(pricePlans);
-
+
// Convert current plan from store to a plan with translations
- const currentPlan = currentPlanFromStore ? plans.find(plan => plan.id === currentPlanFromStore?.id) : null;
+ const currentPlan = currentPlanFromStore
+ ? plans.find((plan) => plan.id === currentPlanFromStore?.id)
+ : null;
const isFreePlan = currentPlan?.isFree || false;
const isLifetimeMember = currentPlan?.isLifetime || false;
// Get subscription price details
- const currentPrice = subscription && currentPlan?.prices.find(
- price => price.priceId === subscription?.priceId
- );
+ const currentPrice =
+ subscription &&
+ currentPlan?.prices.find(
+ (price) => price.priceId === subscription?.priceId
+ );
// Format next billing date if subscription is active
const nextBillingDate = subscription?.currentPeriodEnd
@@ -109,7 +121,11 @@ export default function BillingCard() {
if (!currentPlanFromStore) {
return (
-
+
{t('currentPlan.title')}
{t('currentPlan.description')}
@@ -120,14 +136,8 @@ export default function BillingCard() {
-
-
- {t('upgradePlan')}
-
+
+ {t('upgradePlan')}
@@ -141,23 +151,23 @@ export default function BillingCard() {
return (
-
+
{t('currentPlan.title')}
-
- {t('currentPlan.description')}
-
+ {t('currentPlan.description')}
{/* Plan name and status */}
-
- {currentPlan?.name}
-
+
{currentPlan?.name}
{subscription && (
-
+
{subscription?.status === 'trialing'
? t('status.trial')
: subscription?.status === 'active'
@@ -185,55 +195,48 @@ export default function BillingCard() {
{subscription && currentPrice && (
- {t('price')} {formatPrice(currentPrice.amount, currentPrice.currency)} / {currentPrice.interval === PlanIntervals.MONTH ?
- t('interval.month') :
- currentPrice.interval === PlanIntervals.YEAR ?
- t('interval.year') :
- t('interval.oneTime')}
+ {t('price')}{' '}
+ {formatPrice(currentPrice.amount, currentPrice.currency)} /{' '}
+ {currentPrice.interval === PlanIntervals.MONTH
+ ? t('interval.month')
+ : currentPrice.interval === PlanIntervals.YEAR
+ ? t('interval.year')
+ : t('interval.oneTime')}
{nextBillingDate && (
-
{t('nextBillingDate')} {nextBillingDate}
- )}
-
- {subscription.status === 'trialing' && subscription.currentPeriodEnd && (
-
- {t('trialEnds')} {formatDate(subscription.currentPeriodEnd)}
+
+ {t('nextBillingDate')} {nextBillingDate}
)}
+
+ {subscription.status === 'trialing' &&
+ subscription.currentPeriodEnd && (
+
+ {t('trialEnds')} {formatDate(subscription.currentPeriodEnd)}
+
+ )}
)}
{/* user is on free plan, show upgrade plan button */}
{isFreePlan && (
-
-
- {t('upgradePlan')}
-
+
+ {t('upgradePlan')}
)}
{/* user is lifetime member, show manage billing button */}
{isLifetimeMember && currentUser && (
-
+
{t('manageBilling')}
)}
{/* user has subscription, show manage subscription button */}
{subscription && currentUser && (
-
+
{t('manageSubscription')}
)}
@@ -241,4 +244,4 @@ export default function BillingCard() {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/notification/newsletter-form-card.tsx b/src/components/settings/notification/newsletter-form-card.tsx
index 2c449de..5bd04aa 100644
--- a/src/components/settings/notification/newsletter-form-card.tsx
+++ b/src/components/settings/notification/newsletter-form-card.tsx
@@ -10,7 +10,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import {
Form,
@@ -36,7 +36,7 @@ interface NewsletterFormCardProps {
/**
* Newsletter subscription form card
- *
+ *
* Allows users to toggle their newsletter subscription status
*/
export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
@@ -67,7 +67,9 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
try {
setIsLoading(true);
// Check if the user is already subscribed using server action
- const statusResult = await checkNewsletterStatusAction({ email: currentUser.email });
+ const statusResult = await checkNewsletterStatusAction({
+ email: currentUser.email,
+ });
if (statusResult && statusResult.data?.success) {
const isCurrentlySubscribed = statusResult.data.subscribed;
@@ -116,14 +118,17 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
try {
if (value) {
// Subscribe to newsletter using server action
- const subscribeResult = await subscribeNewsletterAction({ email: currentUser.email });
+ const subscribeResult = await subscribeNewsletterAction({
+ email: currentUser.email,
+ });
if (subscribeResult && subscribeResult.data?.success) {
toast.success(t('newsletter.subscribeSuccess'));
setIsSubscriptionChecked(true);
form.setValue('subscribed', true);
} else {
- const errorMessage = subscribeResult?.data?.error || t('newsletter.subscribeFail');
+ const errorMessage =
+ subscribeResult?.data?.error || t('newsletter.subscribeFail');
toast.error(errorMessage);
setError(errorMessage);
// Reset checkbox if subscription failed
@@ -131,14 +136,17 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
}
} else {
// Unsubscribe from newsletter using server action
- const unsubscribeResult = await unsubscribeNewsletterAction({ email: currentUser.email });
+ const unsubscribeResult = await unsubscribeNewsletterAction({
+ email: currentUser.email,
+ });
if (unsubscribeResult && unsubscribeResult.data?.success) {
toast.success(t('newsletter.unsubscribeSuccess'));
setIsSubscriptionChecked(false);
form.setValue('subscribed', false);
} else {
- const errorMessage = unsubscribeResult?.data?.error || t('newsletter.unsubscribeFail');
+ const errorMessage =
+ unsubscribeResult?.data?.error || t('newsletter.unsubscribeFail');
toast.error(errorMessage);
setError(errorMessage);
// Reset checkbox if unsubscription failed
@@ -157,14 +165,17 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) {
};
return (
-
+
{t('newsletter.title')}
-
- {t('newsletter.description')}
-
+ {t('newsletter.description')}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/profile/update-avatar-card.tsx b/src/components/settings/profile/update-avatar-card.tsx
index 6269b86..b3f3b9b 100644
--- a/src/components/settings/profile/update-avatar-card.tsx
+++ b/src/components/settings/profile/update-avatar-card.tsx
@@ -9,7 +9,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
@@ -126,14 +126,17 @@ export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) {
};
return (
-
+
{t('avatar.title')}
-
- {t('avatar.description')}
-
+ {t('avatar.description')}
@@ -166,4 +169,4 @@ export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/profile/update-name-card.tsx b/src/components/settings/profile/update-name-card.tsx
index 9c306a0..492b0b6 100644
--- a/src/components/settings/profile/update-name-card.tsx
+++ b/src/components/settings/profile/update-name-card.tsx
@@ -8,14 +8,14 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import {
Form,
FormControl,
FormField,
FormItem,
- FormMessage
+ FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { authClient } from '@/lib/auth-client';
@@ -72,7 +72,7 @@ export function UpdateNameCard({ className }: UpdateNameCardProps) {
const onSubmit = async (values: z.infer
) => {
// Don't update if the name hasn't changed
if (values.name === session?.user?.name) {
- console.log("No changes to save");
+ console.log('No changes to save');
return;
}
@@ -103,21 +103,28 @@ export function UpdateNameCard({ className }: UpdateNameCardProps) {
setError(`${ctx.error.status}: ${ctx.error.message}`);
toast.error(t('name.fail'));
},
- });
+ }
+ );
};
return (
-
+
{t('name.title')}
-
- {t('name.description')}
-
+ {t('name.description')}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/security/delete-account-card.tsx b/src/components/settings/security/delete-account-card.tsx
index a27c4bc..eaed889 100644
--- a/src/components/settings/security/delete-account-card.tsx
+++ b/src/components/settings/security/delete-account-card.tsx
@@ -7,7 +7,7 @@ import {
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
- AlertDialogTitle
+ AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import {
@@ -16,7 +16,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import { useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
@@ -27,7 +27,7 @@ import { toast } from 'sonner';
/**
* Delete user account
- *
+ *
* This component allows users to permanently delete their account.
* It includes a confirmation dialog to prevent accidental deletions.
*/
@@ -65,8 +65,8 @@ export function DeleteAccountCard() {
},
onError: (ctx) => {
console.error('delete account error:', ctx.error);
- // { "message": "Session expired. Re-authenticate to perform this action.",
- // "code": "SESSION_EXPIRED_REAUTHENTICATE_TO_PERFORM_THIS_ACTION",
+ // { "message": "Session expired. Re-authenticate to perform this action.",
+ // "code": "SESSION_EXPIRED_REAUTHENTICATE_TO_PERFORM_THIS_ACTION",
// "status": 400, "statusText": "BAD_REQUEST" }
// set freshAge to 0 to disable session refreshness check for user deletion
setError(`${ctx.error.status}: ${ctx.error.message}`);
@@ -77,19 +77,19 @@ export function DeleteAccountCard() {
};
return (
-
+
{t('title')}
-
- {t('description')}
-
+ {t('description')}
-
- {t('warning')}
-
+ {t('warning')}
{error && (
@@ -139,4 +139,4 @@ export function DeleteAccountCard() {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/security/password-card-wrapper.tsx b/src/components/settings/security/password-card-wrapper.tsx
index f4f38af..4949c96 100644
--- a/src/components/settings/security/password-card-wrapper.tsx
+++ b/src/components/settings/security/password-card-wrapper.tsx
@@ -2,12 +2,18 @@
import { ResetPasswordCard } from '@/components/settings/security/reset-password-card';
import { UpdatePasswordCard } from '@/components/settings/security/update-password-card';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { authClient } from '@/lib/auth-client';
+import { cn } from '@/lib/utils';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
-import { cn } from '@/lib/utils';
/**
* PasswordCardWrapper renders either:
@@ -74,14 +80,14 @@ export function PasswordCardWrapper() {
function PasswordSkeletonCard() {
const t = useTranslations('Dashboard.settings.security.updatePassword');
return (
-
+
-
- {t('title')}
-
-
- {t('description')}
-
+ {t('title')}
+ {t('description')}
@@ -93,4 +99,4 @@ function PasswordSkeletonCard() {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/security/reset-password-card.tsx b/src/components/settings/security/reset-password-card.tsx
index 1044bfe..f0c67c2 100644
--- a/src/components/settings/security/reset-password-card.tsx
+++ b/src/components/settings/security/reset-password-card.tsx
@@ -7,7 +7,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import { useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
@@ -20,10 +20,10 @@ interface ResetPasswordCardProps {
/**
* Reset Password Card
- *
+ *
* This component guides users who signed up with social providers
* to set up a password through the forgot password flow.
- *
+ *
* How it works:
* 1. When a user signs in with a social provider, they don't have a password set up
* 2. This component provides a way for them to set up a password using the forgot password flow
@@ -33,7 +33,7 @@ interface ResetPasswordCardProps {
* 6. After setting a password, they can now login with either:
* - Their social provider (as before)
* - Their email and the new password
- *
+ *
* This effectively adds a credential provider to their account, enabling email/password login.
*/
export function ResetPasswordCard({ className }: ResetPasswordCardProps) {
@@ -44,26 +44,27 @@ export function ResetPasswordCard({ className }: ResetPasswordCardProps) {
const handleSetupPassword = () => {
// Pre-fill the email if available to make it easier for the user
if (session?.user?.email) {
- router.push(`/auth/forgot-password?email=${encodeURIComponent(session.user.email)}`);
+ router.push(
+ `/auth/forgot-password?email=${encodeURIComponent(session.user.email)}`
+ );
} else {
router.push('/auth/forgot-password');
}
};
return (
-
+
-
- {t('title')}
-
-
- {t('description')}
-
+ {t('title')}
+ {t('description')}
-
- {t('info')}
-
+ {t('info')}
@@ -72,4 +73,4 @@ export function ResetPasswordCard({ className }: ResetPasswordCardProps) {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/settings/security/update-password-card.tsx b/src/components/settings/security/update-password-card.tsx
index f27c31a..ac59ef3 100644
--- a/src/components/settings/security/update-password-card.tsx
+++ b/src/components/settings/security/update-password-card.tsx
@@ -8,7 +8,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import {
Form,
@@ -16,7 +16,7 @@ import {
FormField,
FormItem,
FormLabel,
- FormMessage
+ FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useLocaleRouter } from '@/i18n/navigation';
@@ -36,12 +36,12 @@ interface UpdatePasswordCardProps {
/**
* Update user password
- *
+ *
* This component allows users to update their password.
- *
+ *
* NOTE: This should only be used for users with credential providers (email/password login).
* For conditional rendering based on provider type, use ConditionalUpdatePasswordCard instead.
- *
+ *
* @see ConditionalUpdatePasswordCard
* @see https://www.better-auth.com/docs/authentication/email-password#update-password
*/
@@ -56,12 +56,8 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
// Create a schema for password validation
const formSchema = z.object({
- currentPassword: z
- .string()
- .min(1, { message: t('currentRequired') }),
- newPassword: z
- .string()
- .min(8, { message: t('newMinLength') }),
+ currentPassword: z.string().min(1, { message: t('currentRequired') }),
+ newPassword: z.string().min(8, { message: t('newMinLength') }),
});
// Initialize the form
@@ -111,30 +107,33 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
setError(`${ctx.error.status}: ${ctx.error.message}`);
toast.error(t('fail'));
},
- });
+ }
+ );
};
return (
-
+
-
- {t('title')}
-
-
- {t('description')}
-
+ {t('title')}
+ {t('description')}
@@ -202,11 +207,13 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
-
- {t('hint')}
-
+ {t('hint')}
-
+
{isSaving ? t('saving') : t('save')}
diff --git a/src/components/shared/built-with-button.tsx b/src/components/shared/built-with-button.tsx
index 117d979..35d82af 100644
--- a/src/components/shared/built-with-button.tsx
+++ b/src/components/shared/built-with-button.tsx
@@ -1,7 +1,7 @@
+import { MkSaaSLogo } from '@/components/layout/logo-mksaas';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import Link from 'next/link';
-import { MkSaaSLogo } from '@/components/layout/logo-mksaas';
export default function BuiltWithButton() {
return (
diff --git a/src/components/shared/custom-mdx-content.tsx b/src/components/shared/custom-mdx-content.tsx
index 23eb165..e549955 100644
--- a/src/components/shared/custom-mdx-content.tsx
+++ b/src/components/shared/custom-mdx-content.tsx
@@ -7,9 +7,9 @@ import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import defaultMdxComponents from 'fumadocs-ui/mdx';
-import type { MDXComponents } from 'mdx/types';
-import { ComponentProps, FC } from 'react';
import * as LucideIcons from 'lucide-react';
+import type { MDXComponents } from 'mdx/types';
+import type { ComponentProps, FC } from 'react';
interface CustomMDXContentProps {
code: string;
@@ -51,17 +51,17 @@ export async function CustomMDXContent({
if (!props.src) {
return null;
}
-
+
return (
-
@@ -71,9 +71,6 @@ export async function CustomMDXContent({
}
return (
-
+
);
-}
\ No newline at end of file
+}
diff --git a/src/components/shared/empty-grid.tsx b/src/components/shared/empty-grid.tsx
index a866dea..9890f88 100644
--- a/src/components/shared/empty-grid.tsx
+++ b/src/components/shared/empty-grid.tsx
@@ -7,7 +7,9 @@ export default function EmptyGrid() {
-
{t('noPostsFound')}
+
+ {t('noPostsFound')}
+
diff --git a/src/components/shared/filter-item-mobile.tsx b/src/components/shared/filter-item-mobile.tsx
index 53399b7..eeb2279 100644
--- a/src/components/shared/filter-item-mobile.tsx
+++ b/src/components/shared/filter-item-mobile.tsx
@@ -1,7 +1,7 @@
'use client';
-import { cn } from '@/lib/utils';
import { LocaleLink } from '@/i18n/navigation';
+import { cn } from '@/lib/utils';
interface FilterItemMobileProps {
title: string;
diff --git a/src/components/waitlist/waitlist-form-card.tsx b/src/components/waitlist/waitlist-form-card.tsx
index 2b8fd8e..36fd911 100644
--- a/src/components/waitlist/waitlist-form-card.tsx
+++ b/src/components/waitlist/waitlist-form-card.tsx
@@ -1,4 +1,4 @@
-"use client";
+'use client';
import { subscribeNewsletterAction } from '@/actions/subscribe-newsletter';
import { FormError } from '@/components/shared/form-error';
@@ -9,7 +9,7 @@ import {
CardDescription,
CardFooter,
CardHeader,
- CardTitle
+ CardTitle,
} from '@/components/ui/card';
import {
Form,
@@ -17,12 +17,12 @@ import {
FormField,
FormItem,
FormLabel,
- FormMessage
+ FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
-import { useTransition, useState } from 'react';
+import { useState, useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
@@ -38,9 +38,7 @@ export function WaitlistFormCard() {
// Create a schema for waitlist form validation
const formSchema = z.object({
- email: z
- .string()
- .email({ message: t('emailValidation') }),
+ email: z.string().email({ message: t('emailValidation') }),
});
// Initialize the form
@@ -80,12 +78,8 @@ export function WaitlistFormCard() {
return (
-
- {t('title')}
-
-
- {t('description')}
-
+ {t('title')}
+ {t('description')}
);
-}
\ No newline at end of file
+}
diff --git a/src/config/avatar-config.tsx b/src/config/avatar-config.tsx
index 02ef1d6..c77d60e 100644
--- a/src/config/avatar-config.tsx
+++ b/src/config/avatar-config.tsx
@@ -1,13 +1,17 @@
'use client';
import { Routes } from '@/routes';
-import { MenuItem } from '@/types';
-import { CreditCardIcon, LayoutDashboardIcon, Settings2Icon } from 'lucide-react';
+import type { MenuItem } from '@/types';
+import {
+ CreditCardIcon,
+ LayoutDashboardIcon,
+ Settings2Icon,
+} from 'lucide-react';
import { useTranslations } from 'next-intl';
/**
* Get avatar config with translations
- *
+ *
* NOTICE: used in client components only
*
* @returns The avatar config with translated titles
diff --git a/src/config/footer-config.tsx b/src/config/footer-config.tsx
index 108180f..e38f412 100644
--- a/src/config/footer-config.tsx
+++ b/src/config/footer-config.tsx
@@ -1,12 +1,12 @@
'use client';
import { Routes } from '@/routes';
-import { NestedMenuItem } from '@/types';
+import type { NestedMenuItem } from '@/types';
import { useTranslations } from 'next-intl';
/**
* Get footer config with translations
- *
+ *
* NOTICE: used in client components only
*
* @returns The footer config with translated titles
diff --git a/src/config/navbar-config.tsx b/src/config/navbar-config.tsx
index 3b5e625..77982c2 100644
--- a/src/config/navbar-config.tsx
+++ b/src/config/navbar-config.tsx
@@ -1,7 +1,7 @@
'use client';
import { Routes } from '@/routes';
-import { NestedMenuItem } from '@/types';
+import type { NestedMenuItem } from '@/types';
import {
AudioLinesIcon,
BuildingIcon,
@@ -30,13 +30,13 @@ import {
ThumbsUpIcon,
UserPlusIcon,
UsersIcon,
- WandSparklesIcon
+ WandSparklesIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
/**
* Get navbar config with translations
- *
+ *
* NOTICE: used in client components only
*
* @returns The navbar config with translated titles and descriptions
diff --git a/src/config/payment-config.tsx b/src/config/payment-config.tsx
index 7807673..45b4d6e 100644
--- a/src/config/payment-config.tsx
+++ b/src/config/payment-config.tsx
@@ -1,12 +1,12 @@
'use client';
+import type { PricePlan } from '@/payment/types';
import { useTranslations } from 'next-intl';
import { websiteConfig } from './website';
-import { PricePlan } from '@/payment/types';
/**
* Get price plans with translations for client components
- *
+ *
* NOTICE: This function should only be used in client components.
* If you need to get the price plans in server components, use getAllPricePlans instead.
* Use this function when showing the pricing table or the billing card to the user.
@@ -28,13 +28,13 @@ export function getPricePlans(): Record {
t('free.features.feature-1'),
t('free.features.feature-2'),
t('free.features.feature-3'),
- t('free.features.feature-4')
+ t('free.features.feature-4'),
],
limits: [
t('free.limits.limit-1'),
t('free.limits.limit-2'),
- t('free.limits.limit-3')
- ]
+ t('free.limits.limit-3'),
+ ],
};
}
@@ -50,10 +50,7 @@ export function getPricePlans(): Record {
t('pro.features.feature-4'),
t('pro.features.feature-5'),
],
- limits: [
- t('pro.limits.limit-1'),
- t('pro.limits.limit-2')
- ]
+ limits: [t('pro.limits.limit-1'), t('pro.limits.limit-2')],
};
}
@@ -69,11 +66,11 @@ export function getPricePlans(): Record {
t('lifetime.features.feature-4'),
t('lifetime.features.feature-5'),
t('lifetime.features.feature-6'),
- t('lifetime.features.feature-7')
+ t('lifetime.features.feature-7'),
],
- limits: []
+ limits: [],
};
}
return plans;
-}
\ No newline at end of file
+}
diff --git a/src/config/sidebar-config.tsx b/src/config/sidebar-config.tsx
index 6ead05e..246b2c9 100644
--- a/src/config/sidebar-config.tsx
+++ b/src/config/sidebar-config.tsx
@@ -1,7 +1,7 @@
'use client';
import { Routes } from '@/routes';
-import { NestedMenuItem } from '@/types';
+import type { NestedMenuItem } from '@/types';
import {
BellIcon,
CircleUserRoundIcon,
@@ -10,7 +10,7 @@ import {
LockKeyholeIcon,
Settings2Icon,
SettingsIcon,
- UsersRoundIcon
+ UsersRoundIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
@@ -41,7 +41,7 @@ export function getSidebarLinks(): NestedMenuItem[] {
icon: ,
href: Routes.AdminUsers,
external: false,
- }
+ },
],
},
{
@@ -71,7 +71,7 @@ export function getSidebarLinks(): NestedMenuItem[] {
icon: ,
href: Routes.SettingsNotifications,
external: false,
- }
+ },
],
},
];
diff --git a/src/config/social-config.tsx b/src/config/social-config.tsx
index 0e56f9d..4a26c40 100644
--- a/src/config/social-config.tsx
+++ b/src/config/social-config.tsx
@@ -1,6 +1,7 @@
'use client';
import { BlueskyIcon } from '@/components/icons/bluesky';
+import { DiscordIcon } from '@/components/icons/discord';
import { FacebookIcon } from '@/components/icons/facebook';
import { GitHubIcon } from '@/components/icons/github';
import { InstagramIcon } from '@/components/icons/instagram';
@@ -8,13 +9,12 @@ import { LinkedInIcon } from '@/components/icons/linkedin';
import { TikTokIcon } from '@/components/icons/tiktok';
import { XTwitterIcon } from '@/components/icons/x';
import { YouTubeIcon } from '@/components/icons/youtube';
-import { MenuItem } from '@/types';
+import type { MenuItem } from '@/types';
import { websiteConfig } from './website';
-import { DiscordIcon } from '@/components/icons/discord';
/**
* Get social config
- *
+ *
* NOTICE: used in client components only
*
* @returns The social config
diff --git a/src/config/website.tsx b/src/config/website.tsx
index 4dbc043..520d8ca 100644
--- a/src/config/website.tsx
+++ b/src/config/website.tsx
@@ -1,5 +1,5 @@
import { PaymentTypes, PlanIntervals } from '@/payment/types';
-import { WebsiteConfig } from '@/types';
+import type { WebsiteConfig } from '@/types';
/**
* website config, without translations
@@ -7,11 +7,11 @@ import { WebsiteConfig } from '@/types';
export const websiteConfig: WebsiteConfig = {
metadata: {
theme: {
- defaultTheme: "default",
+ defaultTheme: 'default',
enableSwitch: true,
},
mode: {
- defaultMode: "system",
+ defaultMode: 'system',
enableSwitch: true,
},
images: {
@@ -29,11 +29,11 @@ export const websiteConfig: WebsiteConfig = {
i18n: {
defaultLocale: 'en',
locales: {
- "en": {
+ en: {
flag: '🇺🇸',
name: 'English',
},
- "zh": {
+ zh: {
flag: '🇨🇳',
name: '中文',
},
@@ -59,26 +59,26 @@ export const websiteConfig: WebsiteConfig = {
provider: 'stripe',
plans: {
free: {
- id: "free",
+ id: 'free',
prices: [],
isFree: true,
isLifetime: false,
},
pro: {
- id: "pro",
+ id: 'pro',
prices: [
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY!,
amount: 990,
- currency: "USD",
+ currency: 'USD',
interval: PlanIntervals.MONTH,
},
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY!,
amount: 9900,
- currency: "USD",
+ currency: 'USD',
interval: PlanIntervals.YEAR,
},
],
@@ -87,18 +87,18 @@ export const websiteConfig: WebsiteConfig = {
recommended: true,
},
lifetime: {
- id: "lifetime",
+ id: 'lifetime',
prices: [
{
type: PaymentTypes.ONE_TIME,
priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_LIFETIME!,
amount: 19900,
- currency: "USD",
+ currency: 'USD',
},
],
isFree: false,
isLifetime: true,
- }
- }
- }
+ },
+ },
+ },
};
diff --git a/src/db/index.ts b/src/db/index.ts
index 2f0de91..be8c802 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -6,7 +6,7 @@ import { drizzle } from 'drizzle-orm/neon-http';
*
* Using the browser-compatible Neon HTTP driver for better compatibility with Next.js
* This avoids the Node.js-specific modules that cause build issues
- *
+ *
* With the neon-http and neon-websockets drivers, you can access a Neon database from serverless environments over HTTP or WebSockets instead of TCP.
* Querying over HTTP is faster for single, non-interactive transactions.
*/
diff --git a/src/hooks/use-clipboard.ts b/src/hooks/use-clipboard.ts
index b19b81e..594caa2 100644
--- a/src/hooks/use-clipboard.ts
+++ b/src/hooks/use-clipboard.ts
@@ -24,4 +24,4 @@ export const useCopyToClipboard = (block: BlockProps) => {
};
return { copied, copy };
-};
\ No newline at end of file
+};
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
index 2b0fe1d..6be5f6c 100644
--- a/src/hooks/use-mobile.ts
+++ b/src/hooks/use-mobile.ts
@@ -1,19 +1,21 @@
-import * as React from "react"
+import * as React from 'react';
-const MOBILE_BREAKPOINT = 768
+const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState(undefined)
+ const [isMobile, setIsMobile] = React.useState(
+ undefined
+ );
React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener('change', onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener('change', onChange);
+ }, []);
- return !!isMobile
+ return !!isMobile;
}
diff --git a/src/hooks/use-payment.ts b/src/hooks/use-payment.ts
index a1ce95c..f88dd48 100644
--- a/src/hooks/use-payment.ts
+++ b/src/hooks/use-payment.ts
@@ -1,22 +1,17 @@
-import { useEffect } from 'react';
-import { usePaymentStore } from '@/stores/payment-store';
import { authClient } from '@/lib/auth-client';
+import { usePaymentStore } from '@/stores/payment-store';
+import { useEffect } from 'react';
/**
* Hook for accessing and managing payment state
- *
+ *
* This hook provides access to the payment state and methods to manage it.
* It also automatically fetches payment information when the user changes.
*/
export function usePayment() {
- const {
- currentPlan,
- subscription,
- isLoading,
- error,
- fetchPayment
- } = usePaymentStore();
-
+ const { currentPlan, subscription, isLoading, error, fetchPayment } =
+ usePaymentStore();
+
const { data: session } = authClient.useSession();
useEffect(() => {
@@ -39,6 +34,6 @@ export function usePayment() {
console.log('refetching payment info for user', currentUser.id);
fetchPayment(currentUser);
}
- }
+ },
};
-}
\ No newline at end of file
+}
diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts
index 2f53467..53a5414 100644
--- a/src/i18n/messages.ts
+++ b/src/i18n/messages.ts
@@ -1,6 +1,6 @@
import deepmerge from 'deepmerge';
+import type { Locale, Messages } from 'next-intl';
import { routing } from './routing';
-import { Locale, Messages } from 'next-intl';
// assume that the default messages are in the en.json file
// if you want to use a different default locale, you can change to other {locale}.json file
diff --git a/src/i18n/request.ts b/src/i18n/request.ts
index 2d49704..d1d89f0 100644
--- a/src/i18n/request.ts
+++ b/src/i18n/request.ts
@@ -16,7 +16,7 @@ import { routing } from './routing';
*/
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
- let requested = await requestLocale;
+ const requested = await requestLocale;
// Ensure that the incoming `locale` is valid
// https://next-intl.dev/blog/next-intl-4-0?s#strictly-typed-locale
diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts
index 25b57d4..58baa73 100644
--- a/src/i18n/routing.ts
+++ b/src/i18n/routing.ts
@@ -1,5 +1,5 @@
-import { defineRouting } from 'next-intl/routing';
import { websiteConfig } from '@/config/website';
+import { defineRouting } from 'next-intl/routing';
export const DEFAULT_LOCALE = websiteConfig.i18n.defaultLocale;
export const LOCALES = Object.keys(websiteConfig.i18n.locales);
diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts
index 801319a..43cc78e 100644
--- a/src/lib/auth-client.ts
+++ b/src/lib/auth-client.ts
@@ -1,6 +1,6 @@
import { adminClient, inferAdditionalFields } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/react';
-import { auth } from './auth';
+import type { auth } from './auth';
import { getBaseUrl } from './urls/urls';
/**
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index bb54c43..fc31a6e 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,16 +1,16 @@
+import { websiteConfig } from '@/config/website';
import db from '@/db/index';
import { account, session, user, verification } from '@/db/schema';
import { defaultMessages } from '@/i18n/messages';
import { LOCALE_COOKIE_NAME, routing } from '@/i18n/routing';
import { sendEmail } from '@/mail';
+import { subscribe } from '@/newsletter';
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { admin } from 'better-auth/plugins';
-import { subscribe } from '@/newsletter';
import { parse as parseCookies } from 'cookie';
-import { Locale } from 'next-intl';
+import type { Locale } from 'next-intl';
import { getUrlWithLocaleInCallbackUrl } from './urls/urls';
-import { websiteConfig } from '@/config/website';
/**
* https://www.better-auth.com/docs/reference/options
@@ -121,7 +121,9 @@ export const auth = betterAuth({
try {
const subscribed = await subscribe(user.email);
if (!subscribed) {
- console.error(`Failed to subscribe user ${user.email} to newsletter`);
+ console.error(
+ `Failed to subscribe user ${user.email} to newsletter`
+ );
} else {
console.log(`User ${user.email} subscribed to newsletter`);
}
@@ -150,7 +152,6 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/concepts/typescript#additional-fields
export type Session = typeof auth.$Infer.Session;
-
/**
* Gets the locale from a request by parsing the cookies
* If no locale is found in the cookies, returns the default locale
@@ -161,4 +162,4 @@ export type Session = typeof auth.$Infer.Session;
export function getLocaleFromRequest(request?: Request): Locale {
const cookies = parseCookies(request?.headers.get('cookie') ?? '');
return (cookies[LOCALE_COOKIE_NAME] as Locale) ?? routing.defaultLocale;
-}
\ No newline at end of file
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index b343aab..39459fd 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,2 +1,2 @@
export const PLACEHOLDER_IMAGE =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==";
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==';
diff --git a/src/lib/docs/i18n.ts b/src/lib/docs/i18n.ts
index fbf48f5..7c8509f 100644
--- a/src/lib/docs/i18n.ts
+++ b/src/lib/docs/i18n.ts
@@ -3,10 +3,10 @@ import type { I18nConfig } from 'fumadocs-core/i18n';
/**
* Internationalization configuration for FumaDocs
- *
+ *
* https://fumadocs.vercel.app/docs/ui/internationalization
*/
export const docsI18nConfig: I18nConfig = {
defaultLanguage: DEFAULT_LOCALE,
languages: LOCALES,
-};
\ No newline at end of file
+};
diff --git a/src/lib/docs/source.ts b/src/lib/docs/source.ts
index cfb6101..8f4fa24 100644
--- a/src/lib/docs/source.ts
+++ b/src/lib/docs/source.ts
@@ -7,7 +7,7 @@ import { docsI18nConfig } from './i18n';
/**
* Turn a content source into a unified interface
- *
+ *
* https://fumadocs.vercel.app/docs/headless/source-api
* https://fumadocs.vercel.app/docs/headless/content-collections
*/
@@ -19,13 +19,13 @@ export const source = loader({
if (!iconName) {
return undefined;
}
-
+
const IconComponent = (LucideIcons as Record)[iconName];
if (IconComponent) {
return createElement(IconComponent);
}
-
+
console.warn(`Icon not found: ${iconName}`);
return undefined;
},
-});
\ No newline at end of file
+});
diff --git a/src/lib/metadata.ts b/src/lib/metadata.ts
index a3b3962..0df345b 100644
--- a/src/lib/metadata.ts
+++ b/src/lib/metadata.ts
@@ -1,8 +1,8 @@
import { websiteConfig } from '@/config/website';
+import { defaultMessages } from '@/i18n/messages';
import { routing } from '@/i18n/routing';
import type { Metadata } from 'next';
import { getBaseUrl } from './urls/urls';
-import { defaultMessages } from '@/i18n/messages';
/**
* Construct the metadata object for the current page (in docs/guides)
diff --git a/src/lib/page/get-page.ts b/src/lib/page/get-page.ts
index 3730986..371107d 100644
--- a/src/lib/page/get-page.ts
+++ b/src/lib/page/get-page.ts
@@ -1,5 +1,5 @@
import { allPages } from 'content-collections';
-import { Locale } from 'next-intl';
+import type { Locale } from 'next-intl';
/**
* Gets a page from the content collection
diff --git a/src/lib/price-plan.ts b/src/lib/price-plan.ts
index 8769249..35263f7 100644
--- a/src/lib/price-plan.ts
+++ b/src/lib/price-plan.ts
@@ -1,5 +1,5 @@
-import { websiteConfig } from "@/config/website";
-import { Price, PricePlan } from "@/payment/types";
+import { websiteConfig } from '@/config/website';
+import type { Price, PricePlan } from '@/payment/types';
/**
* Get all price plans (without translations, like name/description/features)
@@ -16,7 +16,7 @@ export const getAllPricePlans = (): PricePlan[] => {
* @returns Plan or undefined if not found
*/
export const findPlanByPlanId = (planId: string): PricePlan | undefined => {
- return getAllPricePlans().find(plan => plan.id === planId);
+ return getAllPricePlans().find((plan) => plan.id === planId);
};
/**
@@ -27,7 +27,9 @@ export const findPlanByPlanId = (planId: string): PricePlan | undefined => {
export const findPlanByPriceId = (priceId: string): PricePlan | undefined => {
const plans = getAllPricePlans();
for (const plan of plans) {
- const matchingPrice = plan.prices.find(price => price.priceId === priceId);
+ const matchingPrice = plan.prices.find(
+ (price) => price.priceId === priceId
+ );
if (matchingPrice) {
return plan;
}
@@ -41,11 +43,14 @@ export const findPlanByPriceId = (priceId: string): PricePlan | undefined => {
* @param priceId Price ID (Stripe price ID)
* @returns Price or undefined if not found
*/
-export const findPriceInPlan = (planId: string, priceId: string): Price | undefined => {
+export const findPriceInPlan = (
+ planId: string,
+ priceId: string
+): Price | undefined => {
const plan = findPlanByPlanId(planId);
if (!plan) {
console.error(`findPriceInPlan, Plan with ID ${planId} not found`);
return undefined;
}
- return plan.prices.find(price => price.priceId === priceId);
+ return plan.prices.find((price) => price.priceId === priceId);
};
diff --git a/src/lib/release/get-releases.ts b/src/lib/release/get-releases.ts
index a83f2fa..b8c1136 100644
--- a/src/lib/release/get-releases.ts
+++ b/src/lib/release/get-releases.ts
@@ -1,5 +1,5 @@
import { allReleases } from 'content-collections';
-import { Locale } from 'next-intl';
+import type { Locale } from 'next-intl';
/**
* Gets all releases for the changelog page
* @param locale The locale to get releases for
diff --git a/src/lib/server.ts b/src/lib/server.ts
index 5a1a283..d12e739 100644
--- a/src/lib/server.ts
+++ b/src/lib/server.ts
@@ -1,12 +1,12 @@
-import { headers } from "next/headers";
-import { cache } from "react";
-import "server-only";
-import { auth } from "./auth";
+import { headers } from 'next/headers';
+import { cache } from 'react';
+import 'server-only';
+import { auth } from './auth';
export const getSession = cache(async () => {
- const session = await auth.api.getSession({
- headers: await headers(),
- });
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
- return session;
-});
\ No newline at end of file
+ return session;
+});
diff --git a/src/lib/serviceWorker.ts b/src/lib/serviceWorker.ts
index 229e533..861b647 100644
--- a/src/lib/serviceWorker.ts
+++ b/src/lib/serviceWorker.ts
@@ -8,10 +8,10 @@ export function registerServiceWorker() {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
- .then(registration => {
+ .then((registration) => {
console.log('SW registered: ', registration);
})
- .catch(registrationError => {
+ .catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
@@ -25,7 +25,11 @@ type SWMessage = {
};
export function sendMessageToSW(message: SWMessage) {
- if (typeof window !== 'undefined' && 'serviceWorker' in navigator && navigator.serviceWorker.controller) {
+ if (
+ typeof window !== 'undefined' &&
+ 'serviceWorker' in navigator &&
+ navigator.serviceWorker.controller
+ ) {
navigator.serviceWorker.controller.postMessage(message);
}
}
@@ -34,14 +38,14 @@ export function sendMessageToSW(message: SWMessage) {
export function clearIframeCache(url?: string) {
sendMessageToSW({
type: 'CLEAR_IFRAME_CACHE',
- url
+ url,
});
}
// Update the service worker
export function updateServiceWorker() {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
+ navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
}
@@ -61,4 +65,4 @@ export async function isUrlCached(url: string): Promise {
console.error('Error checking cache:', error);
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/urls/urls.ts b/src/lib/urls/urls.ts
index 0fd7b3d..09a3299 100644
--- a/src/lib/urls/urls.ts
+++ b/src/lib/urls/urls.ts
@@ -1,5 +1,5 @@
-import { routing } from "@/i18n/routing";
-import { Locale } from "next-intl";
+import { routing } from '@/i18n/routing';
+import type { Locale } from 'next-intl';
const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ??
@@ -23,24 +23,29 @@ export function shouldAppendLocale(locale?: Locale | null): boolean {
* Get the URL of the application with the locale appended
*/
export function getUrlWithLocale(url: string, locale?: Locale | null): string {
- return shouldAppendLocale(locale) ? `${baseUrl}/${locale}${url}` : `${baseUrl}${url}`;
+ return shouldAppendLocale(locale)
+ ? `${baseUrl}/${locale}${url}`
+ : `${baseUrl}${url}`;
}
/**
* Adds locale to the callbackURL parameter in authentication URLs
- *
+ *
* Example:
* Input: http://localhost:3000/api/auth/reset-password/token?callbackURL=/auth/reset-password
* Output: http://localhost:3000/api/auth/reset-password/token?callbackURL=/zh/auth/reset-password
- *
+ *
* http://localhost:3000/api/auth/verify-email?token=eyJhbGciOiJIUzI1NiJ9&callbackURL=/dashboard
* Output: http://localhost:3000/api/auth/verify-email?token=eyJhbGciOiJIUzI1NiJ9&callbackURL=/zh/dashboard
- *
+ *
* @param url - The original URL with callbackURL parameter
* @param locale - The locale to add to the callbackURL
* @returns The URL with locale added to callbackURL if necessary
*/
-export function getUrlWithLocaleInCallbackUrl(url: string, locale: Locale): string {
+export function getUrlWithLocaleInCallbackUrl(
+ url: string,
+ locale: Locale
+): string {
// If we shouldn't append locale, return original URL
if (!shouldAppendLocale(locale)) {
return url;
@@ -49,23 +54,23 @@ export function getUrlWithLocaleInCallbackUrl(url: string, locale: Locale): stri
try {
// Parse the URL
const urlObj = new URL(url);
-
+
// Check if there's a callbackURL parameter
const callbackURL = urlObj.searchParams.get('callbackURL');
-
+
if (callbackURL) {
// Only modify the callbackURL if it doesn't already include the locale
if (!callbackURL.match(new RegExp(`^/${locale}(/|$)`))) {
// Add locale to the callbackURL
- const localizedCallbackURL = callbackURL.startsWith('/')
- ? `/${locale}${callbackURL}`
+ const localizedCallbackURL = callbackURL.startsWith('/')
+ ? `/${locale}${callbackURL}`
: `/${locale}/${callbackURL}`;
-
+
// Update the search parameter
urlObj.searchParams.set('callbackURL', localizedCallbackURL);
}
}
-
+
return urlObj.toString();
} catch (error) {
// If URL parsing fails, return the original URL
diff --git a/src/mail/components/email-layout.tsx b/src/mail/components/email-layout.tsx
index 339b176..a54e32a 100644
--- a/src/mail/components/email-layout.tsx
+++ b/src/mail/components/email-layout.tsx
@@ -1,4 +1,4 @@
-import { BaseEmailProps } from '@/mail/types';
+import type { BaseEmailProps } from '@/mail/types';
import {
Container,
Font,
@@ -7,7 +7,7 @@ import {
Html,
Section,
Tailwind,
- Text
+ Text,
} from '@react-email/components';
import { createTranslator } from 'use-intl/core';
@@ -20,7 +20,11 @@ interface EmailLayoutProps extends BaseEmailProps {
*
* https://react.email/docs/components/tailwind
*/
-export default function EmailLayout({ locale, messages, children }: EmailLayoutProps) {
+export default function EmailLayout({
+ locale,
+ messages,
+ children,
+}: EmailLayoutProps) {
const t = createTranslator({
locale,
messages,
diff --git a/src/mail/index.ts b/src/mail/index.ts
index d515ec5..fe5e9d3 100644
--- a/src/mail/index.ts
+++ b/src/mail/index.ts
@@ -1,10 +1,16 @@
+import { websiteConfig } from '@/config/website';
import { getMessagesForLocale } from '@/i18n/messages';
import { routing } from '@/i18n/routing';
import { render } from '@react-email/render';
-import { Locale, Messages } from 'next-intl';
+import type { Locale, Messages } from 'next-intl';
import { ResendProvider } from './provider/resend';
-import { EmailTemplate, EmailTemplates, MailProvider, SendRawEmailParams, SendTemplateParams } from './types';
-import { websiteConfig } from '@/config/website';
+import {
+ type EmailTemplate,
+ EmailTemplates,
+ type MailProvider,
+ type SendRawEmailParams,
+ type SendTemplateParams,
+} from './types';
/**
* Global mail provider instance
*/
@@ -31,7 +37,9 @@ export const initializeMailProvider = (): MailProvider => {
if (websiteConfig.mail.provider === 'resend') {
mailProvider = new ResendProvider();
} else {
- throw new Error(`Unsupported mail provider: ${websiteConfig.mail.provider}`);
+ throw new Error(
+ `Unsupported mail provider: ${websiteConfig.mail.provider}`
+ );
}
}
return mailProvider;
@@ -39,7 +47,7 @@ export const initializeMailProvider = (): MailProvider => {
/**
* Send email using the configured mail provider
- *
+ *
* @param params Email parameters
* @returns Success status
*/
diff --git a/src/mail/provider/resend.ts b/src/mail/provider/resend.ts
index cbe8a5b..c37de14 100644
--- a/src/mail/provider/resend.ts
+++ b/src/mail/provider/resend.ts
@@ -1,6 +1,11 @@
import { websiteConfig } from '@/config/website';
-import { MailProvider, SendEmailResult, SendRawEmailParams, SendTemplateParams } from '@/mail/types';
import { getTemplate } from '@/mail';
+import type {
+ MailProvider,
+ SendEmailResult,
+ SendRawEmailParams,
+ SendTemplateParams,
+} from '@/mail/types';
import { Resend } from 'resend';
/**
@@ -17,9 +22,11 @@ export class ResendProvider implements MailProvider {
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY environment variable is not set.');
}
-
+
if (!websiteConfig.mail.from) {
- throw new Error('Default from email address is not set in websiteConfig.');
+ throw new Error(
+ 'Default from email address is not set in websiteConfig.'
+ );
}
const apiKey = process.env.RESEND_API_KEY;
@@ -40,7 +47,9 @@ export class ResendProvider implements MailProvider {
* @param params Parameters for sending a templated email
* @returns Send result
*/
- public async sendTemplate(params: SendTemplateParams): Promise {
+ public async sendTemplate(
+ params: SendTemplateParams
+ ): Promise {
const { to, template, context, locale } = params;
try {
@@ -72,11 +81,18 @@ export class ResendProvider implements MailProvider {
* @param params Parameters for sending a raw email
* @returns Send result
*/
- public async sendRawEmail(params: SendRawEmailParams): Promise {
+ public async sendRawEmail(
+ params: SendRawEmailParams
+ ): Promise {
const { to, subject, html, text } = params;
if (!this.from || !to || !subject || !html) {
- console.warn('Missing required fields for email send', { from: this.from, to, subject, html });
+ console.warn('Missing required fields for email send', {
+ from: this.from,
+ to,
+ subject,
+ html,
+ });
return {
success: false,
error: 'Missing required fields',
diff --git a/src/mail/templates/contact-message.tsx b/src/mail/templates/contact-message.tsx
index 354b9ff..db1e6d4 100644
--- a/src/mail/templates/contact-message.tsx
+++ b/src/mail/templates/contact-message.tsx
@@ -11,8 +11,18 @@ interface ContactMessageProps extends BaseEmailProps {
message: string;
}
-export function ContactMessage({ name, email, message, locale, messages }: ContactMessageProps) {
- const t = createTranslator({ locale, messages, namespace: 'Mail.contactMessage' });
+export function ContactMessage({
+ name,
+ email,
+ message,
+ locale,
+ messages,
+}: ContactMessageProps) {
+ const t = createTranslator({
+ locale,
+ messages,
+ namespace: 'Mail.contactMessage',
+ });
return (
diff --git a/src/mail/templates/forgot-password.tsx b/src/mail/templates/forgot-password.tsx
index 42d2370..3176729 100644
--- a/src/mail/templates/forgot-password.tsx
+++ b/src/mail/templates/forgot-password.tsx
@@ -11,16 +11,23 @@ interface ForgotPasswordProps extends BaseEmailProps {
name: string;
}
-export function ForgotPassword({ url, name, locale, messages, }: ForgotPasswordProps) {
- const t = createTranslator({ locale, messages, namespace: 'Mail.forgotPassword' });
-
+export function ForgotPassword({
+ url,
+ name,
+ locale,
+ messages,
+}: ForgotPasswordProps) {
+ const t = createTranslator({
+ locale,
+ messages,
+ namespace: 'Mail.forgotPassword',
+ });
+
return (
{t('title', { name })}
{t('body')}
-
- {t('resetPassword')}
-
+ {t('resetPassword')}
);
}
diff --git a/src/mail/templates/subscribe-newsletter.tsx b/src/mail/templates/subscribe-newsletter.tsx
index a2412be..9fcabd0 100644
--- a/src/mail/templates/subscribe-newsletter.tsx
+++ b/src/mail/templates/subscribe-newsletter.tsx
@@ -5,17 +5,21 @@ import type { BaseEmailProps } from '@/mail/types';
import { Heading, Text } from '@react-email/components';
import { createTranslator } from 'use-intl/core';
-interface SubscribeNewsletterProps extends BaseEmailProps {
-}
+interface SubscribeNewsletterProps extends BaseEmailProps {}
-export function SubscribeNewsletter({ locale, messages }: SubscribeNewsletterProps) {
- const t = createTranslator({ locale, messages, namespace: 'Mail.subscribeNewsletter' });
+export function SubscribeNewsletter({
+ locale,
+ messages,
+}: SubscribeNewsletterProps) {
+ const t = createTranslator({
+ locale,
+ messages,
+ namespace: 'Mail.subscribeNewsletter',
+ });
return (
-
- {t('subject')}
-
+ {t('subject')}
{t('body')}
);
diff --git a/src/mail/templates/verify-email.tsx b/src/mail/templates/verify-email.tsx
index c30ad42..e8ebcbc 100644
--- a/src/mail/templates/verify-email.tsx
+++ b/src/mail/templates/verify-email.tsx
@@ -12,8 +12,12 @@ interface VerifyEmailProps extends BaseEmailProps {
}
export function VerifyEmail({ url, name, locale, messages }: VerifyEmailProps) {
- const t = createTranslator({ locale, messages, namespace: 'Mail.verifyEmail' });
-
+ const t = createTranslator({
+ locale,
+ messages,
+ namespace: 'Mail.verifyEmail',
+ });
+
return (
{t('title', { name })}
diff --git a/src/mail/types.ts b/src/mail/types.ts
index 634e1aa..79998f7 100644
--- a/src/mail/types.ts
+++ b/src/mail/types.ts
@@ -1,8 +1,8 @@
-import { Locale, Messages } from 'next-intl';
-import { VerifyEmail } from './templates/verify-email';
+import type { Locale, Messages } from 'next-intl';
+import { ContactMessage } from './templates/contact-message';
import { ForgotPassword } from './templates/forgot-password';
import { SubscribeNewsletter } from './templates/subscribe-newsletter';
-import { ContactMessage } from './templates/contact-message';
+import { VerifyEmail } from './templates/verify-email';
/**
* list all the email templates here
diff --git a/src/middleware.ts b/src/middleware.ts
index 37ad8b4..9d824dd 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,8 +1,12 @@
import createMiddleware from 'next-intl/middleware';
-import { NextRequest, NextResponse } from 'next/server';
+import { type NextRequest, NextResponse } from 'next/server';
import { LOCALES, routing } from './i18n/routing';
import { getSession } from './lib/server';
-import { DEFAULT_LOGIN_REDIRECT, protectedRoutes, routesNotAllowedByLoggedInUsers } from './routes';
+import {
+ DEFAULT_LOGIN_REDIRECT,
+ protectedRoutes,
+ routesNotAllowedByLoggedInUsers,
+} from './routes';
const intlMiddleware = createMiddleware(routing);
@@ -15,18 +19,27 @@ export default async function middleware(req: NextRequest) {
// console.log('middleware, isLoggedIn', isLoggedIn);
// Get the pathname of the request (e.g. /zh/dashboard to /dashboard)
- const pathnameWithoutLocale = getPathnameWithoutLocale(nextUrl.pathname, LOCALES);
+ const pathnameWithoutLocale = getPathnameWithoutLocale(
+ nextUrl.pathname,
+ LOCALES
+ );
// If the route can not be accessed by logged in users, redirect if the user is logged in
if (isLoggedIn) {
- const isNotAllowedRoute = routesNotAllowedByLoggedInUsers.some(route => new RegExp(`^${route}$`).test(pathnameWithoutLocale));
+ const isNotAllowedRoute = routesNotAllowedByLoggedInUsers.some((route) =>
+ new RegExp(`^${route}$`).test(pathnameWithoutLocale)
+ );
if (isNotAllowedRoute) {
- console.log('<< middleware end, not allowed route, already logged in, redirecting to dashboard');
+ console.log(
+ '<< middleware end, not allowed route, already logged in, redirecting to dashboard'
+ );
return NextResponse.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
}
}
- const isProtectedRoute = protectedRoutes.some(route => new RegExp(`^${route}$`).test(pathnameWithoutLocale));
+ const isProtectedRoute = protectedRoutes.some((route) =>
+ new RegExp(`^${route}$`).test(pathnameWithoutLocale)
+ );
// console.log('middleware, isProtectedRoute', isProtectedRoute);
// If the route is a protected route, redirect to login if user is not logged in
@@ -36,9 +49,12 @@ export default async function middleware(req: NextRequest) {
callbackUrl += nextUrl.search;
}
const encodedCallbackUrl = encodeURIComponent(callbackUrl);
- console.log('<< middleware end, not logged in, redirecting to login, callbackUrl', callbackUrl);
+ console.log(
+ '<< middleware end, not logged in, redirecting to login, callbackUrl',
+ callbackUrl
+ );
return NextResponse.redirect(
- new URL(`/auth/login?callbackUrl=${encodedCallbackUrl}`, nextUrl),
+ new URL(`/auth/login?callbackUrl=${encodedCallbackUrl}`, nextUrl)
);
}
@@ -63,7 +79,7 @@ function getPathnameWithoutLocale(pathname: string, locales: string[]): string {
*/
export const config = {
// The `matcher` is relative to the `basePath`
- matcher: [
+ matcher: [
// Match all pathnames except for
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
diff --git a/src/newsletter/index.ts b/src/newsletter/index.ts
index dcc9ea3..f889128 100644
--- a/src/newsletter/index.ts
+++ b/src/newsletter/index.ts
@@ -1,6 +1,6 @@
import { websiteConfig } from '@/config/website';
import { ResendNewsletterProvider } from './provider/resend';
-import { NewsletterProvider } from './types';
+import type { NewsletterProvider } from './types';
/**
* Global newsletter provider instance
@@ -27,7 +27,9 @@ export const initializeNewsletterProvider = (): NewsletterProvider => {
if (websiteConfig.newsletter.provider === 'resend') {
newsletterProvider = new ResendNewsletterProvider();
} else {
- throw new Error(`Unsupported newsletter provider: ${websiteConfig.newsletter.provider}`);
+ throw new Error(
+ `Unsupported newsletter provider: ${websiteConfig.newsletter.provider}`
+ );
}
}
diff --git a/src/newsletter/provider/resend.ts b/src/newsletter/provider/resend.ts
index 0a1e325..7c4b700 100644
--- a/src/newsletter/provider/resend.ts
+++ b/src/newsletter/provider/resend.ts
@@ -1,5 +1,10 @@
import { sendEmail } from '@/mail';
-import { CheckSubscribeStatusParams, NewsletterProvider, SubscribeNewsletterParams, UnsubscribeNewsletterParams } from '@/newsletter/types';
+import type {
+ CheckSubscribeStatusParams,
+ NewsletterProvider,
+ SubscribeNewsletterParams,
+ UnsubscribeNewsletterParams,
+} from '@/newsletter/types';
import { Resend } from 'resend';
/**
@@ -37,7 +42,9 @@ export class ResendNewsletterProvider implements NewsletterProvider {
async subscribe({ email }: SubscribeNewsletterParams): Promise {
try {
// First, list all contacts to find the one with the matching email
- const listResult = await this.resend.contacts.list({ audienceId: this.audienceId });
+ const listResult = await this.resend.contacts.list({
+ audienceId: this.audienceId,
+ });
if (listResult.error) {
console.error('Error listing contacts:', listResult.error);
return false;
@@ -47,7 +54,7 @@ export class ResendNewsletterProvider implements NewsletterProvider {
// Check if the contact with the given email exists in the list
let contact = null;
if (listResult.data?.data && Array.isArray(listResult.data.data)) {
- contact = listResult.data.data.find(c => c.email === email);
+ contact = listResult.data.data.find((c) => c.email === email);
}
// console.log('subscribe params:', { email, contact });
@@ -130,10 +137,14 @@ export class ResendNewsletterProvider implements NewsletterProvider {
* @param email The email address to check
* @returns True if the user is subscribed, false otherwise
*/
- async checkSubscribeStatus({ email }: CheckSubscribeStatusParams): Promise {
+ async checkSubscribeStatus({
+ email,
+ }: CheckSubscribeStatusParams): Promise {
try {
// First, list all contacts to find the one with the matching email
- const listResult = await this.resend.contacts.list({ audienceId: this.audienceId });
+ const listResult = await this.resend.contacts.list({
+ audienceId: this.audienceId,
+ });
if (listResult.error) {
console.error('Error listing contacts:', listResult.error);
@@ -143,8 +154,8 @@ export class ResendNewsletterProvider implements NewsletterProvider {
// console.log('check newsletter params:', { email, listResult });
// Check if the contact with the given email exists in the list
if (listResult.data?.data && Array.isArray(listResult.data.data)) {
- return listResult.data.data.some(contact =>
- contact.email === email && contact.unsubscribed === false
+ return listResult.data.data.some(
+ (contact) => contact.email === email && contact.unsubscribed === false
);
}
diff --git a/src/newsletter/types.ts b/src/newsletter/types.ts
index f07e7b7..b0997de 100644
--- a/src/newsletter/types.ts
+++ b/src/newsletter/types.ts
@@ -10,11 +10,17 @@ export interface CheckSubscribeStatusParams {
email: string;
}
-export type SubscribeNewsletterHandler = (params: SubscribeNewsletterParams) => Promise;
+export type SubscribeNewsletterHandler = (
+ params: SubscribeNewsletterParams
+) => Promise;
-export type UnsubscribeNewsletterHandler = (params: UnsubscribeNewsletterParams) => Promise;
+export type UnsubscribeNewsletterHandler = (
+ params: UnsubscribeNewsletterParams
+) => Promise;
-export type CheckSubscribeStatusHandler = (params: CheckSubscribeStatusParams) => Promise;
+export type CheckSubscribeStatusHandler = (
+ params: CheckSubscribeStatusParams
+) => Promise;
/**
* Newsletter provider, currently only Resend is supported
diff --git a/src/payment/index.ts b/src/payment/index.ts
index fc25e40..0a3d09f 100644
--- a/src/payment/index.ts
+++ b/src/payment/index.ts
@@ -1,6 +1,14 @@
-import { websiteConfig } from "@/config/website";
-import { StripeProvider } from "./provider/stripe";
-import { CheckoutResult, CreateCheckoutParams, CreatePortalParams, PaymentProvider, PortalResult, Subscription, getSubscriptionsParams } from "./types";
+import { websiteConfig } from '@/config/website';
+import { StripeProvider } from './provider/stripe';
+import type {
+ CheckoutResult,
+ CreateCheckoutParams,
+ CreatePortalParams,
+ PaymentProvider,
+ PortalResult,
+ Subscription,
+ getSubscriptionsParams,
+} from './types';
/**
* Global payment provider instance
@@ -28,7 +36,9 @@ export const initializePaymentProvider = (): PaymentProvider => {
if (websiteConfig.payment.provider === 'stripe') {
paymentProvider = new StripeProvider();
} else {
- throw new Error(`Unsupported payment provider: ${websiteConfig.payment.provider}`);
+ throw new Error(
+ `Unsupported payment provider: ${websiteConfig.payment.provider}`
+ );
}
}
return paymentProvider;
diff --git a/src/payment/provider/stripe.ts b/src/payment/provider/stripe.ts
index a8bcba2..e715aeb 100644
--- a/src/payment/provider/stripe.ts
+++ b/src/payment/provider/stripe.ts
@@ -1,16 +1,31 @@
+import { randomUUID } from 'crypto';
import db from '@/db';
import { payment, session, user } from '@/db/schema';
-import { findPlanByPriceId, findPriceInPlan, findPlanByPlanId } from '@/lib/price-plan';
-import { randomUUID } from 'crypto';
+import {
+ findPlanByPlanId,
+ findPlanByPriceId,
+ findPriceInPlan,
+} from '@/lib/price-plan';
import { desc, eq } from 'drizzle-orm';
import { Stripe } from 'stripe';
-import { CheckoutResult, CreateCheckoutParams, CreatePortalParams, getSubscriptionsParams, PaymentProvider, PaymentStatus, PaymentTypes, PlanInterval, PlanIntervals, PortalResult, Subscription } from '../types';
+import {
+ type CheckoutResult,
+ type CreateCheckoutParams,
+ type CreatePortalParams,
+ type PaymentProvider,
+ type PaymentStatus,
+ PaymentTypes,
+ type PlanInterval,
+ PlanIntervals,
+ type PortalResult,
+ type Subscription,
+ type getSubscriptionsParams,
+} from '../types';
/**
* Stripe payment provider implementation
*/
export class StripeProvider implements PaymentProvider {
-
private stripe: Stripe;
private webhookSecret: string;
@@ -39,7 +54,10 @@ export class StripeProvider implements PaymentProvider {
* @param name Optional customer name
* @returns Stripe customer ID
*/
- private async createOrGetCustomer(email: string, name?: string): Promise {
+ private async createOrGetCustomer(
+ email: string,
+ name?: string
+ ): Promise {
try {
// Search for existing customer
const customers = await this.stripe.customers.list({
@@ -56,16 +74,18 @@ export class StripeProvider implements PaymentProvider {
// user does not exist, update user with customer id
// in case you deleted user in database, but forgot to delete customer in Stripe
if (!userId) {
- console.log(`User ${email} does not exist, update with customer id ${customerId}`);
+ console.log(
+ `User ${email} does not exist, update with customer id ${customerId}`
+ );
await this.updateUserWithCustomerId(customerId, email);
}
return customerId;
}
// Create new customer
- const customer = await this.stripe.customers.create({
+ const customer = await this.stripe.customers.create({
email,
- name: name || undefined
+ name: name || undefined,
});
// Update user record in database with the new customer ID
@@ -84,14 +104,17 @@ export class StripeProvider implements PaymentProvider {
* @param email Customer email
* @returns Promise that resolves when the update is complete
*/
- private async updateUserWithCustomerId(customerId: string, email: string): Promise {
+ private async updateUserWithCustomerId(
+ customerId: string,
+ email: string
+ ): Promise {
try {
// Update user record with customer ID if email matches
const result = await db
.update(user)
.set({
customerId: customerId,
- updatedAt: new Date()
+ updatedAt: new Date(),
})
.where(eq(user.email, email))
.returning({ id: user.id });
@@ -112,7 +135,9 @@ export class StripeProvider implements PaymentProvider {
* @param customerId Stripe customer ID
* @returns User ID or undefined if not found
*/
- private async findUserIdByCustomerId(customerId: string): Promise {
+ private async findUserIdByCustomerId(
+ customerId: string
+ ): Promise {
try {
// Query the user table for a matching customerId
const result = await db
@@ -139,8 +164,18 @@ export class StripeProvider implements PaymentProvider {
* @param params Parameters for creating the checkout session
* @returns Checkout result
*/
- public async createCheckout(params: CreateCheckoutParams): Promise {
- const { planId, priceId, customerEmail, successUrl, cancelUrl, metadata, locale } = params;
+ public async createCheckout(
+ params: CreateCheckoutParams
+ ): Promise {
+ const {
+ planId,
+ priceId,
+ customerEmail,
+ successUrl,
+ cancelUrl,
+ metadata,
+ locale,
+ } = params;
try {
// Get plan and price
@@ -159,7 +194,10 @@ export class StripeProvider implements PaymentProvider {
const userName = metadata?.userName;
// Create or get customer
- const customerId = await this.createOrGetCustomer(customerEmail, userName);
+ const customerId = await this.createOrGetCustomer(
+ customerEmail,
+ userName
+ );
// Add planId and priceId to metadata, so we can get it in the webhook event
const customMetadata = {
@@ -169,15 +207,18 @@ export class StripeProvider implements PaymentProvider {
};
// Set up the line items
- const lineItems = [{
- price: priceId,
- quantity: 1,
- }];
+ const lineItems = [
+ {
+ price: priceId,
+ quantity: 1,
+ },
+ ];
// Create checkout session parameters
const checkoutParams: Stripe.Checkout.SessionCreateParams = {
line_items: lineItems,
- mode: price.type === PaymentTypes.SUBSCRIPTION ? 'subscription' : 'payment',
+ mode:
+ price.type === PaymentTypes.SUBSCRIPTION ? 'subscription' : 'payment',
success_url: successUrl ?? '',
cancel_url: cancelUrl ?? '',
metadata: customMetadata,
@@ -188,7 +229,9 @@ export class StripeProvider implements PaymentProvider {
// Add locale if provided
if (locale) {
- checkoutParams.locale = this.mapLocaleToStripeLocale(locale) as Stripe.Checkout.SessionCreateParams.Locale;
+ checkoutParams.locale = this.mapLocaleToStripeLocale(
+ locale
+ ) as Stripe.Checkout.SessionCreateParams.Locale;
}
// Add payment intent data for one-time payments
@@ -211,12 +254,14 @@ export class StripeProvider implements PaymentProvider {
// Add trial period if applicable
if (price.trialPeriodDays && price.trialPeriodDays > 0) {
- checkoutParams.subscription_data.trial_period_days = price.trialPeriodDays;
+ checkoutParams.subscription_data.trial_period_days =
+ price.trialPeriodDays;
}
}
// Create the checkout session
- const session = await this.stripe.checkout.sessions.create(checkoutParams);
+ const session =
+ await this.stripe.checkout.sessions.create(checkoutParams);
return {
url: session.url!,
@@ -233,14 +278,20 @@ export class StripeProvider implements PaymentProvider {
* @param params Parameters for creating the portal
* @returns Portal result
*/
- public async createCustomerPortal(params: CreatePortalParams): Promise {
+ public async createCustomerPortal(
+ params: CreatePortalParams
+ ): Promise {
const { customerId, returnUrl, locale } = params;
try {
const session = await this.stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl ?? '',
- locale: locale ? this.mapLocaleToStripeLocale(locale) as Stripe.BillingPortal.SessionCreateParams.Locale : undefined,
+ locale: locale
+ ? (this.mapLocaleToStripeLocale(
+ locale
+ ) as Stripe.BillingPortal.SessionCreateParams.Locale)
+ : undefined,
});
return {
@@ -257,7 +308,9 @@ export class StripeProvider implements PaymentProvider {
* @param params Parameters for getting subscriptions
* @returns Array of subscription objects
*/
- public async getSubscriptions(params: getSubscriptionsParams): Promise {
+ public async getSubscriptions(
+ params: getSubscriptionsParams
+ ): Promise {
const { userId } = params;
try {
@@ -269,7 +322,7 @@ export class StripeProvider implements PaymentProvider {
.orderBy(desc(payment.createdAt)); // Sort by creation date, newest first
// Map database records to our subscription model
- return subscriptions.map(subscription => ({
+ return subscriptions.map((subscription) => ({
id: subscription.subscriptionId || '',
customerId: subscription.customerId,
priceId: subscription.priceId,
@@ -294,7 +347,10 @@ export class StripeProvider implements PaymentProvider {
* @param payload Raw webhook payload
* @param signature Webhook signature
*/
- public async handleWebhookEvent(payload: string, signature: string): Promise {
+ public async handleWebhookEvent(
+ payload: string,
+ signature: string
+ ): Promise {
try {
// Verify the event signature if webhook secret is available
const event = this.stripe.webhooks.constructEvent(
@@ -345,21 +401,29 @@ export class StripeProvider implements PaymentProvider {
* Create payment record
* @param stripeSubscription Stripe subscription
*/
- private async onCreateSubscription(stripeSubscription: Stripe.Subscription): Promise {
- console.log(`>> Create payment record for Stripe subscription ${stripeSubscription.id}`);
+ private async onCreateSubscription(
+ stripeSubscription: Stripe.Subscription
+ ): Promise {
+ console.log(
+ `>> Create payment record for Stripe subscription ${stripeSubscription.id}`
+ );
const customerId = stripeSubscription.customer as string;
// get priceId from subscription items (this is always available)
const priceId = stripeSubscription.items.data[0]?.price.id;
if (!priceId) {
- console.warn(`<< No priceId found for subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No priceId found for subscription ${stripeSubscription.id}`
+ );
return;
}
// get userId from metadata, we add it in the createCheckout session
const userId = stripeSubscription.metadata.userId;
if (!userId) {
- console.warn(`<< No userId found for subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No userId found for subscription ${stripeSubscription.id}`
+ );
return;
}
@@ -372,18 +436,24 @@ export class StripeProvider implements PaymentProvider {
customerId: customerId,
subscriptionId: stripeSubscription.id,
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
- status: this.mapSubscriptionStatusToPaymentStatus(stripeSubscription.status),
- periodStart: stripeSubscription.current_period_start ?
- new Date(stripeSubscription.current_period_start * 1000) : null,
- periodEnd: stripeSubscription.current_period_end ?
- new Date(stripeSubscription.current_period_end * 1000) : null,
+ status: this.mapSubscriptionStatusToPaymentStatus(
+ stripeSubscription.status
+ ),
+ periodStart: stripeSubscription.current_period_start
+ ? new Date(stripeSubscription.current_period_start * 1000)
+ : null,
+ periodEnd: stripeSubscription.current_period_end
+ ? new Date(stripeSubscription.current_period_end * 1000)
+ : null,
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
- trialStart: stripeSubscription.trial_start ?
- new Date(stripeSubscription.trial_start * 1000) : null,
- trialEnd: stripeSubscription.trial_end ?
- new Date(stripeSubscription.trial_end * 1000) : null,
+ trialStart: stripeSubscription.trial_start
+ ? new Date(stripeSubscription.trial_start * 1000)
+ : null,
+ trialEnd: stripeSubscription.trial_end
+ ? new Date(stripeSubscription.trial_end * 1000)
+ : null,
createdAt: new Date(),
- updatedAt: new Date()
+ updatedAt: new Date(),
};
const result = await db
@@ -392,9 +462,13 @@ export class StripeProvider implements PaymentProvider {
.returning({ id: payment.id });
if (result.length > 0) {
- console.log(`<< Created new payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}`);
+ console.log(
+ `<< Created new payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}`
+ );
} else {
- console.warn(`<< No payment record created for Stripe subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No payment record created for Stripe subscription ${stripeSubscription.id}`
+ );
}
}
@@ -402,13 +476,19 @@ export class StripeProvider implements PaymentProvider {
* Update payment record
* @param stripeSubscription Stripe subscription
*/
- private async onUpdateSubscription(stripeSubscription: Stripe.Subscription): Promise {
- console.log(`>> Update payment record for Stripe subscription ${stripeSubscription.id}`);
+ private async onUpdateSubscription(
+ stripeSubscription: Stripe.Subscription
+ ): Promise {
+ console.log(
+ `>> Update payment record for Stripe subscription ${stripeSubscription.id}`
+ );
// get priceId from subscription items (this is always available)
const priceId = stripeSubscription.items.data[0]?.price.id;
if (!priceId) {
- console.warn(`<< No priceId found for subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No priceId found for subscription ${stripeSubscription.id}`
+ );
return;
}
@@ -416,17 +496,23 @@ export class StripeProvider implements PaymentProvider {
const updateFields: any = {
priceId: priceId,
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
- status: this.mapSubscriptionStatusToPaymentStatus(stripeSubscription.status),
- periodStart: stripeSubscription.current_period_start ?
- new Date(stripeSubscription.current_period_start * 1000) : undefined,
- periodEnd: stripeSubscription.current_period_end ?
- new Date(stripeSubscription.current_period_end * 1000) : undefined,
+ status: this.mapSubscriptionStatusToPaymentStatus(
+ stripeSubscription.status
+ ),
+ periodStart: stripeSubscription.current_period_start
+ ? new Date(stripeSubscription.current_period_start * 1000)
+ : undefined,
+ periodEnd: stripeSubscription.current_period_end
+ ? new Date(stripeSubscription.current_period_end * 1000)
+ : undefined,
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
- trialStart: stripeSubscription.trial_start ?
- new Date(stripeSubscription.trial_start * 1000) : undefined,
- trialEnd: stripeSubscription.trial_end ?
- new Date(stripeSubscription.trial_end * 1000) : undefined,
- updatedAt: new Date()
+ trialStart: stripeSubscription.trial_start
+ ? new Date(stripeSubscription.trial_start * 1000)
+ : undefined,
+ trialEnd: stripeSubscription.trial_end
+ ? new Date(stripeSubscription.trial_end * 1000)
+ : undefined,
+ updatedAt: new Date(),
};
const result = await db
@@ -436,9 +522,13 @@ export class StripeProvider implements PaymentProvider {
.returning({ id: payment.id });
if (result.length > 0) {
- console.log(`<< Updated payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}`);
+ console.log(
+ `<< Updated payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}`
+ );
} else {
- console.warn(`<< No payment record found for Stripe subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No payment record found for Stripe subscription ${stripeSubscription.id}`
+ );
}
}
@@ -446,21 +536,31 @@ export class StripeProvider implements PaymentProvider {
* Update payment record, set status to canceled
* @param stripeSubscription Stripe subscription
*/
- private async onDeleteSubscription(stripeSubscription: Stripe.Subscription): Promise {
- console.log(`>> Mark payment record for Stripe subscription ${stripeSubscription.id} as canceled`);
+ private async onDeleteSubscription(
+ stripeSubscription: Stripe.Subscription
+ ): Promise {
+ console.log(
+ `>> Mark payment record for Stripe subscription ${stripeSubscription.id} as canceled`
+ );
const result = await db
.update(payment)
.set({
- status: this.mapSubscriptionStatusToPaymentStatus(stripeSubscription.status),
- updatedAt: new Date()
+ status: this.mapSubscriptionStatusToPaymentStatus(
+ stripeSubscription.status
+ ),
+ updatedAt: new Date(),
})
.where(eq(payment.subscriptionId, stripeSubscription.id))
.returning({ id: payment.id });
if (result.length > 0) {
- console.log(`<< Marked payment record for subscription ${stripeSubscription.id} as canceled`);
+ console.log(
+ `<< Marked payment record for subscription ${stripeSubscription.id} as canceled`
+ );
} else {
- console.warn(`<< No payment record found to cancel for Stripe subscription ${stripeSubscription.id}`);
+ console.warn(
+ `<< No payment record found to cancel for Stripe subscription ${stripeSubscription.id}`
+ );
}
}
@@ -468,7 +568,9 @@ export class StripeProvider implements PaymentProvider {
* Handle one-time payment
* @param session Stripe checkout session
*/
- private async onOnetimePayment(session: Stripe.Checkout.Session): Promise {
+ private async onOnetimePayment(
+ session: Stripe.Checkout.Session
+ ): Promise {
const customerId = session.customer as string;
console.log(`>> Handle onetime payment for customer ${customerId}`);
@@ -505,10 +607,14 @@ export class StripeProvider implements PaymentProvider {
.returning({ id: payment.id });
if (result.length === 0) {
- console.warn(`<< Failed to create one-time payment record for user ${userId}`);
+ console.warn(
+ `<< Failed to create one-time payment record for user ${userId}`
+ );
return;
} else {
- console.log(`<< Created one-time payment record for user ${userId}, price: ${priceId}`);
+ console.log(
+ `<< Created one-time payment record for user ${userId}, price: ${priceId}`
+ );
}
}
@@ -517,7 +623,9 @@ export class StripeProvider implements PaymentProvider {
* @param subscription Stripe subscription
* @returns PlanInterval
*/
- private mapStripeIntervalToPlanInterval(subscription: Stripe.Subscription): PlanInterval {
+ private mapStripeIntervalToPlanInterval(
+ subscription: Stripe.Subscription
+ ): PlanInterval {
switch (subscription.items.data[0]?.plan.interval) {
case 'month':
return PlanIntervals.MONTH;
@@ -534,7 +642,9 @@ export class StripeProvider implements PaymentProvider {
* @param status Stripe subscription status
* @returns PaymentStatus
*/
- private mapSubscriptionStatusToPaymentStatus(status: Stripe.Subscription.Status): PaymentStatus {
+ private mapSubscriptionStatusToPaymentStatus(
+ status: Stripe.Subscription.Status
+ ): PaymentStatus {
const statusMap: Record = {
active: 'active',
canceled: 'canceled',
@@ -555,13 +665,43 @@ export class StripeProvider implements PaymentProvider {
* @returns Stripe locale string
*/
private mapLocaleToStripeLocale(locale: string): string {
- // Stripe supported locales as of 2023:
+ // Stripe supported locales as of 2023:
// https://stripe.com/docs/js/appendix/supported_locales
const stripeLocales = [
- 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'et', 'fi', 'fil',
- 'fr', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'ms',
- 'mt', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sv',
- 'th', 'tr', 'vi', 'zh'
+ 'bg',
+ 'cs',
+ 'da',
+ 'de',
+ 'el',
+ 'en',
+ 'es',
+ 'et',
+ 'fi',
+ 'fil',
+ 'fr',
+ 'hr',
+ 'hu',
+ 'id',
+ 'it',
+ 'ja',
+ 'ko',
+ 'lt',
+ 'lv',
+ 'ms',
+ 'mt',
+ 'nb',
+ 'nl',
+ 'pl',
+ 'pt',
+ 'ro',
+ 'ru',
+ 'sk',
+ 'sl',
+ 'sv',
+ 'th',
+ 'tr',
+ 'vi',
+ 'zh',
];
// First check if the exact locale is supported
@@ -578,4 +718,4 @@ export class StripeProvider implements PaymentProvider {
// Default to auto to let Stripe detect the language
return 'auto';
}
-}
\ No newline at end of file
+}
diff --git a/src/payment/types.ts b/src/payment/types.ts
index a87ec12..c879ed3 100644
--- a/src/payment/types.ts
+++ b/src/payment/types.ts
@@ -1,4 +1,4 @@
-import { Locale } from 'next-intl';
+import type { Locale } from 'next-intl';
/**
* Interval types for subscription plans
diff --git a/src/routes.ts b/src/routes.ts
index f0ddfb6..feb1941 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -7,7 +7,7 @@ export enum Routes {
// marketing pages
FAQ = '/#faq',
Features = '/#features',
- Pricing = '/pricing', // change to /#pricing if you want to use the pricing section in homepage
+ Pricing = '/pricing', // change to /#pricing if you want to use the pricing section in homepage
Blog = '/blog',
Docs = '/docs',
About = '/about',
@@ -63,10 +63,7 @@ export enum Routes {
/**
* The routes that can not be accessed by logged in users
*/
-export const routesNotAllowedByLoggedInUsers = [
- Routes.Login,
- Routes.Register,
-];
+export const routesNotAllowedByLoggedInUsers = [Routes.Login, Routes.Register];
/**
* The routes that are protected and require authentication
diff --git a/src/storage/config/storage-config.ts b/src/storage/config/storage-config.ts
index b3094b0..97a5927 100644
--- a/src/storage/config/storage-config.ts
+++ b/src/storage/config/storage-config.ts
@@ -1,8 +1,8 @@
-import { StorageConfig } from '../types';
+import type { StorageConfig } from '../types';
/**
* Default storage configuration
- *
+ *
* This configuration is loaded from environment variables
*/
export const storageConfig: StorageConfig = {
@@ -13,4 +13,4 @@ export const storageConfig: StorageConfig = {
bucketName: process.env.STORAGE_BUCKET_NAME || '',
publicUrl: process.env.STORAGE_PUBLIC_URL,
forcePathStyle: process.env.STORAGE_FORCE_PATH_STYLE !== 'false',
-};
\ No newline at end of file
+};
diff --git a/src/storage/index.ts b/src/storage/index.ts
index 90403c5..7f9bf3f 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -1,7 +1,7 @@
import { websiteConfig } from '@/config/website';
import { storageConfig } from './config/storage-config';
import { S3Provider } from './provider/s3';
-import { StorageConfig, StorageProvider, UploadFileResult } from './types';
+import type { StorageConfig, StorageProvider, UploadFileResult } from './types';
const API_STORAGE_UPLOAD = '/api/storage/upload';
const API_STORAGE_PRESIGNED_URL = '/api/storage/presigned-url';
@@ -38,7 +38,9 @@ export const initializeStorageProvider = (): StorageProvider => {
if (websiteConfig.storage.provider === 's3') {
storageProvider = new S3Provider();
} else {
- throw new Error(`Unsupported storage provider: ${websiteConfig.storage.provider}`);
+ throw new Error(
+ `Unsupported storage provider: ${websiteConfig.storage.provider}`
+ );
}
}
return storageProvider;
@@ -46,7 +48,7 @@ export const initializeStorageProvider = (): StorageProvider => {
/**
* Uploads a file to the configured storage provider
- *
+ *
* @param file - The file to upload (Buffer or Blob)
* @param filename - Original filename with extension
* @param contentType - MIME type of the file
@@ -65,7 +67,7 @@ export const uploadFile = async (
/**
* Deletes a file from the storage provider
- *
+ *
* @param key - The storage key of the file to delete
* @returns Promise that resolves when the file is deleted
*/
@@ -76,7 +78,7 @@ export const deleteFile = async (key: string): Promise => {
/**
* Generates a pre-signed URL for direct browser uploads
- *
+ *
* @param filename - Filename with extension
* @param contentType - MIME type of the file
* @param folder - Optional folder path to store the file in
@@ -87,16 +89,21 @@ export const getPresignedUploadUrl = async (
filename: string,
contentType: string,
folder?: string,
- expiresIn: number = 3600
+ expiresIn = 3600
): Promise => {
const provider = getStorageProvider();
- return provider.getPresignedUploadUrl({ filename, contentType, folder, expiresIn });
+ return provider.getPresignedUploadUrl({
+ filename,
+ contentType,
+ folder,
+ expiresIn,
+ });
};
/**
* Uploads a file from the browser to the storage provider
* This function is meant to be used in client components
- *
+ *
* @param file - The file object from an input element
* @param folder - Optional folder path to store the file in
* @returns Promise with the URL of the uploaded file
@@ -176,7 +183,10 @@ export const uploadFileFromBrowser = async (
return await fileUrlResponse.json();
}
} catch (error) {
- const message = error instanceof Error ? error.message : 'Unknown error occurred during file upload';
+ const message =
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred during file upload';
throw new Error(message);
}
};
diff --git a/src/storage/provider/s3.ts b/src/storage/provider/s3.ts
index 7c5e9a2..6260de2 100644
--- a/src/storage/provider/s3.ts
+++ b/src/storage/provider/s3.ts
@@ -1,21 +1,25 @@
-import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
-import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'crypto';
+import {
+ GetObjectCommand,
+ PutObjectCommand,
+ S3Client,
+} from '@aws-sdk/client-s3';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { storageConfig } from '../config/storage-config';
import {
ConfigurationError,
- PresignedUploadUrlParams,
- StorageConfig,
+ type PresignedUploadUrlParams,
+ type StorageConfig,
StorageError,
- StorageProvider,
+ type StorageProvider,
UploadError,
- UploadFileParams,
- UploadFileResult
+ type UploadFileParams,
+ type UploadFileResult,
} from '../types';
/**
* Amazon S3 storage provider implementation
- *
+ *
* This provider works with Amazon S3 and compatible services like Cloudflare R2
* https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html
* https://www.npmjs.com/package/@aws-sdk/client-s3
@@ -44,7 +48,8 @@ export class S3Provider implements StorageProvider {
return this.s3Client;
}
- const { region, endpoint, accessKeyId, secretAccessKey, forcePathStyle } = this.config;
+ const { region, endpoint, accessKeyId, secretAccessKey, forcePathStyle } =
+ this.config;
if (!region) {
throw new ConfigurationError('Storage region is not configured');
@@ -136,7 +141,10 @@ export class S3Provider implements StorageProvider {
throw error;
}
- const message = error instanceof Error ? error.message : 'Unknown error occurred during file upload';
+ const message =
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred during file upload';
console.error('uploadFile, error', message);
throw new UploadError(message);
}
@@ -159,12 +167,17 @@ export class S3Provider implements StorageProvider {
Key: key,
};
- await s3.send(new PutObjectCommand({
- ...command,
- Body: '',
- }));
+ await s3.send(
+ new PutObjectCommand({
+ ...command,
+ Body: '',
+ })
+ );
} catch (error) {
- const message = error instanceof Error ? error.message : 'Unknown error occurred during file deletion';
+ const message =
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred during file deletion';
console.error('deleteFile, error', message);
throw new StorageError(message);
}
@@ -173,7 +186,9 @@ export class S3Provider implements StorageProvider {
/**
* Generate a pre-signed URL for direct browser uploads
*/
- public async getPresignedUploadUrl(params: PresignedUploadUrlParams): Promise {
+ public async getPresignedUploadUrl(
+ params: PresignedUploadUrlParams
+ ): Promise {
try {
const { filename, contentType, folder, expiresIn = 3600 } = params;
const s3 = this.getS3Client();
@@ -195,7 +210,10 @@ export class S3Provider implements StorageProvider {
const url = await getSignedUrl(s3, command, { expiresIn });
return { url, key };
} catch (error) {
- const message = error instanceof Error ? error.message : 'Unknown error occurred while generating presigned URL';
+ const message =
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred while generating presigned URL';
console.error('getPresignedUploadUrl, error', message);
throw new StorageError(message);
}
diff --git a/src/storage/types.ts b/src/storage/types.ts
index cac0762..179098e 100644
--- a/src/storage/types.ts
+++ b/src/storage/types.ts
@@ -80,10 +80,12 @@ export interface StorageProvider {
/**
* Generate a pre-signed URL for client-side uploads
*/
- getPresignedUploadUrl(params: PresignedUploadUrlParams): Promise;
+ getPresignedUploadUrl(
+ params: PresignedUploadUrlParams
+ ): Promise;
/**
* Get the provider's name
*/
getProviderName(): string;
-}
\ No newline at end of file
+}
diff --git a/src/stores/locale-store.ts b/src/stores/locale-store.ts
index 7ead825..b3a0108 100644
--- a/src/stores/locale-store.ts
+++ b/src/stores/locale-store.ts
@@ -1,4 +1,4 @@
-import { Locale } from 'next-intl';
+import type { Locale } from 'next-intl';
import { create } from 'zustand';
interface LocaleState {
diff --git a/src/stores/payment-store.ts b/src/stores/payment-store.ts
index 0bccff0..70b07c6 100644
--- a/src/stores/payment-store.ts
+++ b/src/stores/payment-store.ts
@@ -1,8 +1,8 @@
import { getActiveSubscriptionAction } from '@/actions/get-active-subscription';
import { getLifetimeStatusAction } from '@/actions/get-lifetime-status';
-import { Session } from '@/lib/auth';
+import type { Session } from '@/lib/auth';
import { getAllPricePlans } from '@/lib/price-plan';
-import { PricePlan, Subscription } from '@/payment/types';
+import type { PricePlan, Subscription } from '@/payment/types';
import { create } from 'zustand';
/**
@@ -13,7 +13,7 @@ export interface PaymentState {
currentPlan: PricePlan | null;
// Active subscription
subscription: Subscription | null;
- // Loading state
+ // Loading state
isLoading: boolean;
// Error state
error: string | null;
@@ -47,7 +47,7 @@ export const usePaymentStore = create((set, get) => ({
set({
currentPlan: null,
subscription: null,
- error: null
+ error: null,
});
return;
}
@@ -56,9 +56,9 @@ export const usePaymentStore = create((set, get) => ({
set({ isLoading: true, error: null });
// Get all price plans
- let plans: PricePlan[] = getAllPricePlans();
- const freePlan = plans.find(plan => plan.isFree);
- const lifetimePlan = plans.find(plan => plan.isLifetime);
+ const plans: PricePlan[] = getAllPricePlans();
+ const freePlan = plans.find((plan) => plan.isFree);
+ const lifetimePlan = plans.find((plan) => plan.isLifetime);
// Check if user is a lifetime member directly from the database
let isLifetimeMember = false;
@@ -89,7 +89,7 @@ export const usePaymentStore = create((set, get) => ({
currentPlan: lifetimePlan || null,
subscription: null,
isLoading: false,
- error: null
+ error: null,
});
return;
}
@@ -102,36 +102,52 @@ export const usePaymentStore = create((set, get) => ({
// Set subscription state
if (activeSubscription) {
- const plan = plans.find(p => p.prices.find(price =>
- price.priceId === activeSubscription.priceId)) || null;
- console.log('subscription found, setting plan for user', user.id, plan?.id);
+ const plan =
+ plans.find((p) =>
+ p.prices.find(
+ (price) => price.priceId === activeSubscription.priceId
+ )
+ ) || null;
+ console.log(
+ 'subscription found, setting plan for user',
+ user.id,
+ plan?.id
+ );
set({
currentPlan: plan,
subscription: activeSubscription,
isLoading: false,
- error: null
+ error: null,
});
- } else { // No subscription found - set to free plan
- console.log('no subscription found, setting free plan for user', user.id);
+ } else {
+ // No subscription found - set to free plan
+ console.log(
+ 'no subscription found, setting free plan for user',
+ user.id
+ );
set({
currentPlan: freePlan || null,
subscription: null,
isLoading: false,
- error: null
+ error: null,
});
}
- } else { // Failed to fetch subscription
- console.error('fetch subscription for user failed', result?.data?.error);
+ } else {
+ // Failed to fetch subscription
+ console.error(
+ 'fetch subscription for user failed',
+ result?.data?.error
+ );
set({
error: result?.data?.error || 'Failed to fetch payment data',
- isLoading: false
+ isLoading: false,
});
}
} catch (error) {
console.error('fetch payment data error:', error);
set({
error: 'Failed to fetch payment data',
- isLoading: false
+ isLoading: false,
});
} finally {
set({ isLoading: false });
@@ -146,7 +162,7 @@ export const usePaymentStore = create((set, get) => ({
currentPlan: null,
subscription: null,
isLoading: false,
- error: null
+ error: null,
});
- }
-}));
\ No newline at end of file
+ },
+}));
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 9d53602..7db6b1f 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,4 +1,4 @@
-@import 'tailwindcss';
+@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@@ -171,7 +171,6 @@
--sidebar-ring: oklch(0.552 0.016 285.938);
}
-
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
@@ -211,7 +210,7 @@ body {
* 2. we suggest always using the default theme for better user experience,
* and then override the .theme-default to customize the theme
*/
-.theme-default{
+.theme-default {
/* default theme */
}
@@ -253,4 +252,4 @@ body {
--primary: var(--color-amber-500);
--primary-foreground: var(--color-amber-50);
}
-}
\ No newline at end of file
+}
diff --git a/src/styles/mdx.css b/src/styles/mdx.css
index 2542406..70d4807 100644
--- a/src/styles/mdx.css
+++ b/src/styles/mdx.css
@@ -1,8 +1,8 @@
-@import 'tailwindcss';
+@import "tailwindcss";
/* Fumadocs Themes */
/* https://fumadocs.vercel.app/docs/ui/theme#themes */
-@import 'fumadocs-ui/css/neutral.css';
-@import 'fumadocs-ui/css/preset.css';
+@import "fumadocs-ui/css/neutral.css";
+@import "fumadocs-ui/css/preset.css";
@source '../../node_modules/fumadocs-ui/dist/**/*.js';
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index 5e14454..2fae781 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -24,12 +24,12 @@ export interface MetadataConfig {
}
export interface ModeConfig {
- defaultMode?: "light" | "dark" | "system"; // The default mode of the website
+ defaultMode?: 'light' | 'dark' | 'system'; // The default mode of the website
enableSwitch?: boolean; // Whether to enable the mode switch
}
export interface ThemeConfig {
- defaultTheme?: "default" | "blue" | "green" | "amber" | "neutral"; // The default theme of the website
+ defaultTheme?: 'default' | 'blue' | 'green' | 'amber' | 'neutral'; // The default theme of the website
enableSwitch?: boolean; // Whether to enable the theme switch
}
diff --git a/vercel.json b/vercel.json
index bdc6d64..7cf7c1f 100644
--- a/vercel.json
+++ b/vercel.json
@@ -4,4 +4,4 @@
"maxDuration": 60
}
}
-}
\ No newline at end of file
+}