42 | {page.title} 43 |
44 | {page.description && ( 45 |{page.description}
46 | )} 47 |49 |
{page.description}
46 | )} 47 |Follow Me
103 | 104 | 110 |Star on GitHub
112 | 113 |119 | {formatNumber({ value: Number(usage) + currentUses })} Excel formulas 120 | generated so far. 121 |
122 | 223 |32 | Please select your login method. 33 |
34 |37 | By clicking continue, you agree to our{" "} 38 | 42 | Privacy Policy 43 | 44 | . 45 |
46 |69 | {user.email} 70 |
71 | )} 72 |152 | {subscriptionPlan.isCanceled 153 | ? "Your plan will be canceled on " 154 | : "Your plan renews on "} 155 | {dayjs(subscriptionPlan.membershipExpire).format( 156 | "YYYY-MM-DD HH:mm" 157 | )} 158 | . 159 |
160 | ) : null} 161 |{text}
} 17 |*]:text-muted-foreground", 90 | className 91 | )} 92 | {...props} 93 | /> 94 | ), 95 | img: ({ 96 | className, 97 | alt, 98 | ...props 99 | }: React.ImgHTMLAttributes) => ( 100 | // eslint-disable-next-line @next/next/no-img-element 101 | 102 | ), 103 | hr: ({ ...props }) =>
, 104 | table: ({ className, ...props }: React.HTMLAttributes) => ( 105 | 106 |108 | ), 109 | tr: ({ className, ...props }: React.HTMLAttributes107 |
) => ( 110 | 114 | ), 115 | th: ({ className, ...props }) => ( 116 | 123 | ), 124 | td: ({ className, ...props }) => ( 125 | 132 | ), 133 | pre: ({ className, ...props }) => ( 134 | 141 | ), 142 | code: ({ className, ...props }) => ( 143 | 150 | ), 151 | Image, 152 | Callout, 153 | Card: MdxCard, 154 | }; 155 | 156 | interface MdxProps { 157 | code: string; 158 | } 159 | 160 | export function Mdx({ code }: MdxProps) { 161 | const Component = useMDXComponent(code); 162 | 163 | return ( 164 |
165 |167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /components/social-icons/icons.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react' 2 | 3 | // Icons taken from: https://simpleicons.org/ 4 | // To add a new icon, add a new function here and add it to components in social-icons/index.tsx 5 | 6 | export function Facebook(svgProps: SVGProps166 | ) { 7 | return ( 8 | 11 | ) 12 | } 13 | 14 | export function Github(svgProps: SVGProps ) { 15 | return ( 16 | 19 | ) 20 | } 21 | 22 | export function Linkedin(svgProps: SVGProps ) { 23 | return ( 24 | 27 | ) 28 | } 29 | 30 | export function Mail(svgProps: SVGProps ) { 31 | return ( 32 | 36 | ) 37 | } 38 | 39 | export function Twitter(svgProps: SVGProps ) { 40 | return ( 41 | 44 | ) 45 | } 46 | 47 | export function TwitterX(svgProps: SVGProps ) { 48 | return ( 49 | 55 | ) 56 | } 57 | 58 | export function Youtube(svgProps: SVGProps ) { 59 | return ( 60 | 63 | ) 64 | } 65 | 66 | export function Mastodon(svgProps: SVGProps ) { 67 | return ( 68 | 71 | ) 72 | } 73 | 74 | export function Threads(svgProps: SVGProps ) { 75 | return ( 76 | 79 | ) 80 | } 81 | 82 | export function Instagram(svgProps: SVGProps ) { 83 | return ( 84 | 87 | ) 88 | } 89 | 90 | export function WeChat(svgProps: SVGProps ) { 91 | return ( 92 | 102 | ) 103 | } 104 | 105 | export function JueJin(svgProps: SVGProps ) { 106 | return ( 107 | 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /components/social-icons/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Facebook, 3 | Github, 4 | Instagram, 5 | JueJin, 6 | Linkedin, 7 | Mail, 8 | Mastodon, 9 | Threads, 10 | Twitter, 11 | TwitterX, 12 | WeChat, 13 | Youtube 14 | } from './icons' 15 | 16 | const components = { 17 | mail: Mail, 18 | github: Github, 19 | facebook: Facebook, 20 | youtube: Youtube, 21 | linkedin: Linkedin, 22 | twitter: Twitter, 23 | twitterX: TwitterX, 24 | weChat: WeChat, 25 | jueJin: JueJin, 26 | mastodon: Mastodon, 27 | threads: Threads, 28 | instagram: Instagram 29 | } 30 | 31 | type SocialIconProps = { 32 | kind: keyof typeof components 33 | href: string | undefined 34 | size?: number 35 | } 36 | 37 | const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => { 38 | if ( 39 | !href || 40 | (kind === 'mail' && 41 | !/^mailto:\w+([.-]?\w+)@\w+([.-]?\w+)(.\w{2,3})+$/.test(href)) 42 | ) 43 | return null 44 | 45 | const SocialSvg = components[kind] 46 | 47 | return ( 48 | 54 | {kind} 55 | 58 | 59 | ) 60 | } 61 | 62 | export default SocialIcon 63 | -------------------------------------------------------------------------------- /components/subscribe/Subscribe.tsx: -------------------------------------------------------------------------------- 1 | import SubscribeCard from "@/components/subscribe/SubscribeCard"; 2 | import { axios } from "@/lib/axios"; 3 | import { 4 | BOOST_PACK_CREDITS, 5 | BOOST_PACK_EXPIRE, 6 | SINGLE_VARIANT_KEY, 7 | SUBSCRIPTION_VARIANT_KEY, 8 | } from "@/lib/constants"; 9 | import { CreateCheckoutResponse, SubscribeInfo } from "@/types/subscribe"; 10 | import { UserInfo } from "@/types/user"; 11 | import { toast } from "react-hot-toast"; 12 | 13 | export const subscribeInfo: SubscribeInfo = { 14 | free: { 15 | title: "Free", 16 | description: "Begin Your Exploration Journey", 17 | amount: 0, 18 | expireType: "day", 19 | possess: [ 20 | `${ 21 | process.env.NEXT_PUBLIC_COMMON_USER_DAILY_LIMIT_STR || "10" 22 | } free credits per day`, 23 | "Optional credits purchase", 24 | ], 25 | }, 26 | membership: { 27 | isPopular: true, 28 | title: "Premium", 29 | description: "50x more credits than Free version", 30 | amount: 4.99, 31 | expireType: "month", 32 | possess: [ 33 | "Up to 500 credits per day", 34 | "Optional credits purchase", 35 | "Early access to new features", 36 | ], 37 | buttonText: "Upgrade Now", 38 | mainClassName: "purple-500", 39 | buttonClassName: "bg-gradient-to-r from-pink-500 to-purple-500", 40 | }, 41 | boostPack: { 42 | title: "Boost Pack", 43 | description: "Enough for a worry-free week", 44 | amount: Number(process.env.NEXT_PUBLIC_BOOST_PACK_PRICE || "0"), 45 | // expireType: "", 46 | possess: [ 47 | "One-off buy", 48 | `${BOOST_PACK_CREDITS || "100"} credits ${ 49 | BOOST_PACK_EXPIRE / 3600 / 24 50 | }-day validity`, 51 | "No auto-renewal after expiry", 52 | ], 53 | buttonText: `Get ${BOOST_PACK_CREDITS || "100"} credits`, 54 | }, 55 | }; 56 | 57 | export default function Subscribe({ user }: { user: UserInfo | null }) { 58 | const getStartFreeVersion = () => { 59 | window.scrollTo({ top: 0, behavior: "smooth" }); 60 | }; 61 | const subscribe = async () => { 62 | if (!user || !user.userId) { 63 | toast.error("Please login first"); 64 | return; 65 | } 66 | try { 67 | const { checkoutURL } = await axios.post ( 68 | "/api/payment/subscribe", 69 | { 70 | userId: user.userId, 71 | type: SUBSCRIPTION_VARIANT_KEY, 72 | }, 73 | { 74 | headers: { 75 | token: user.accessToken, 76 | }, 77 | } 78 | ); 79 | window.location.href = checkoutURL; 80 | // window.open(checkoutURL, "_blank", "noopener, noreferrer"); 81 | } catch (err) { 82 | console.log(err); 83 | } 84 | }; 85 | const purchase = async () => { 86 | if (!user || !user.userId) { 87 | toast.error("Please login first"); 88 | return; 89 | } 90 | console.log("purchase"); 91 | try { 92 | const { checkoutURL } = await axios.post ( 93 | "/api/payment/subscribe", 94 | { 95 | userId: user.userId, 96 | type: SINGLE_VARIANT_KEY, 97 | }, 98 | { 99 | headers: { 100 | token: user.accessToken, 101 | }, 102 | } 103 | ); 104 | window.location.href = checkoutURL; 105 | } catch (e) { 106 | console.log(e); 107 | } 108 | }; 109 | 110 | return ( 111 | 112 |141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /components/subscribe/SubscribeCard.tsx: -------------------------------------------------------------------------------- 1 | import PossessPoint from "@/components/icons/PossessPoint"; 2 | import clsxm from "@/lib/clsxm"; 3 | import { Subscription } from "@/types/subscribe"; 4 | import { Button } from "../ui/button"; 5 | 6 | interface IProps { 7 | id?: string; 8 | info: Subscription; 9 | clickButton: () => void; 10 | } 11 | 12 | export default function SubscribeCard({ id, info, clickButton }: IProps) { 13 | return ( 14 |113 |115 |UPGRADE
114 |116 | 135 | {/*117 |134 |118 |133 |122 | 127 | 132 | */} 140 | 21 | {info.isPopular ? ( 22 |58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef23 | Popular 24 |25 | ) : ( 26 | <>> 27 | )} 28 |29 |49 |{info.title}
30 |{info.description}31 |32 | ${info.amount} 33 | {info.expireType ? `/ ${info.expireType}` : ""} 34 |35 |36 | {info.possess.length ? ( 37 | info.possess.map((i) => { 38 | return ( 39 |
48 |- 40 |
42 | ); 43 | }) 44 | ) : ( 45 | <>> 46 | )} 47 |{i} 41 | 50 | 56 |57 |, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef , 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes36 | 44 | ) => ( 52 | 59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes ) => ( 66 | 73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef , 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef , 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef , 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef , 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 | 32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 | 44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 | 56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef , 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef , 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef , 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes , 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef ( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 | 29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 | 44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 | 56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 | 64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 | 76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef , 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef38 | , 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef , 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef73 | , 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef , 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef110 | 112 | 113 | {children} 114 |111 | , 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef133 | 135 | 136 | {children} 137 |134 | , 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef , 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes ) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes ) { 7 | return ( 8 | 12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /config/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { DashboardConfig } from "@/types/sideNav"; 2 | 3 | export const dashboardConfig: DashboardConfig = { 4 | mainNav: [ 5 | 6 | ], 7 | sidebarNav: [ 8 | // { 9 | // title: "History", 10 | // href: "/history", 11 | // icon: "post", 12 | // }, 13 | { 14 | title: "Billing", 15 | href: "/billing", 16 | icon: "billing", 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | import { SiteConfig } from "@/types/siteConfig" 2 | 3 | const baseSiteConfig = { 4 | name: "Smart Excel", 5 | description: 6 | "Generate the Excel formulas in seconds.", 7 | url: "https://smartexcel.cc", 8 | ogImage: "https://smartexcel.cc/og.jpg", 9 | metadataBase: new URL("https://www.smartexcel.cc"), 10 | keywords: ["SmartExcel", "ChatGPT", "Excel formulas", "Excel AI", "文心一言", "智谱"], 11 | authors: [ 12 | { 13 | name: "weijunext", 14 | url: "https://weijunext.com", 15 | } 16 | ], 17 | creator: '@weijunext', 18 | themeColor: '#fff', 19 | icons: { 20 | icon: "/favicon.ico", 21 | shortcut: "/favicon-16x16.png", 22 | apple: "/apple-touch-icon.png", 23 | }, 24 | links: { 25 | twitter: "https://x.com/weijunext", 26 | github: "https://github.com/weijunext/smartexcel", 27 | }, 28 | } 29 | 30 | export const siteConfig: SiteConfig = { 31 | ...baseSiteConfig, 32 | openGraph: { 33 | type: "website", 34 | locale: "en_US", 35 | url: baseSiteConfig.url, 36 | title: baseSiteConfig.name, 37 | description: baseSiteConfig.description, 38 | siteName: baseSiteConfig.name, 39 | }, 40 | twitter: { 41 | card: "summary_large_image", 42 | title: baseSiteConfig.name, 43 | description: baseSiteConfig.description, 44 | images: [`${baseSiteConfig.url}/og.png`], 45 | creator: baseSiteConfig.creator, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /content/about/privacy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Privacy 3 | description: The Privacy Policy for Taxonomy App. 4 | --- 5 | 6 | Last updated: November 03, 2023 7 | 8 | This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. 9 | 10 | We use Your Personal data to provide and improve the Service. By using the Servic, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the [Privacy Policy Generator](https://www.privacypolicies.com/privacy-policy-generator/). 11 | 12 | ## Interpretation and Definitions 13 | 14 | ### Interpretation 15 | 16 | The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural. 17 | 18 | ### Definitions 19 | 20 | For the purposes of this Privacy Policy: 21 | 22 | - **Account** means a unique account created for You to access our Service or parts of our Service. 23 | - **Affiliate** means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority. 24 | 25 | - **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Smart Excel. 26 | 27 | - **Cookies** are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses. 28 | 29 | - **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet. 30 | 31 | - **Personal Data** is any information that relates to an identified or identifiable individual. 32 | 33 | - **Service** refers to the Website. 34 | 35 | - **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used. 36 | 37 | - **Third-party Social Media Service** refers to any website or any social network website through which a User can log in or create an account to use the Service. 38 | 39 | - **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit). 40 | 41 | - **Website** refers to Smart Excel, accessible from [https://smartexcel.cc](https://smartexcel.cc) 42 | 43 | - **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable. 44 | 45 | ## Collecting and Using Your Personal Data 46 | 47 | ### Types of Data Collected 48 | 49 | #### Personal Data 50 | 51 | While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to: 52 | 53 | - Email address 54 | 55 | - Usage Data 56 | 57 | #### Usage Data 58 | 59 | Usage Data is collected automatically when using the Service. 60 | 61 | Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data. 62 | 63 | When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data. 64 | 65 | We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device. 66 | 67 | #### Information from Third-Party Social Media Services 68 | 69 | The Company allows You to create an account and log in to use the Service through the following Third-party Social Media Services: 70 | 71 | - Google 72 | - Github 73 | 74 | If You decide to register through or otherwise grant us access to a Third-Party Social Media Service, We may collect Personal data that is already associated with Your Third-Party Social Media Service's account, such as Your name, Your email address, Your activities or Your contact list associated with that account. 75 | 76 | You may also have the option of sharing additional information with the Company through Your Third-Party Social Media Service's account. If You choose to provide such information and Personal Data, during registration or otherwise, You are giving the Company permission to use, share, and store it in a manner consistent with this Privacy Policy. 77 | 78 | #### Tracking Technologies and Cookies 79 | 80 | We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include: 81 | 82 | - **Cookies or Browser Cookies.** A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies. 83 | - **Web Beacons.** Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity). 84 | 85 | Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. Learn more about cookies on the [Privacy Policies website](https://www.privacypolicies.com/blog/privacy-policy-template/#Use_Of_Cookies_Log_Files_And_Tracking) article. 86 | 87 | We use both Session and Persistent Cookies for the purposes set out below: 88 | 89 | - **Necessary / Essential Cookies** 90 | 91 | Type: Session Cookies 92 | 93 | Administered by: Us 94 | 95 | Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services. 96 | 97 | - **Cookies Policy / Notice Acceptance Cookies** 98 | 99 | Type: Persistent Cookies 100 | 101 | Administered by: Us 102 | 103 | Purpose: These Cookies identify if users have accepted the use of cookies on the Website. 104 | 105 | - **Functionality Cookies** 106 | 107 | Type: Persistent Cookies 108 | 109 | Administered by: Us 110 | 111 | Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website. 112 | 113 | For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy. 114 | 115 | ### Use of Your Personal Data 116 | 117 | The Company may use Personal Data for the following purposes: 118 | 119 | - **To provide and maintain our Service**, including to monitor the usage of our Service. 120 | - **To manage Your Account:** to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user. 121 | - **For the performance of a contract:** the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service. 122 | - **To contact You:** To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation. 123 | - **To provide You** with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information. 124 | - **To manage Your requests:** To attend and manage Your requests to Us. 125 | 126 | - **For business transfers:** We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred. 127 | - **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience. 128 | 129 | We may share Your personal information in the following situations: 130 | 131 | - **With Service Providers:** We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You. 132 | - **For business transfers:** We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company. 133 | - **With Affiliates:** We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us. 134 | - **With business partners:** We may share Your information with Our business partners to offer You certain products, services or promotions. 135 | - **With other users:** when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile. 136 | - **With Your consent**: We may disclose Your personal information for any other purpose with Your consent. 137 | 138 | ### Retention of Your Personal Data 139 | 140 | The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. 141 | 142 | The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods. 143 | 144 | ### Transfer of Your Personal Data 145 | 146 | Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction. 147 | 148 | Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer. 149 | 150 | The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information. 151 | 152 | ### Delete Your Personal Data 153 | 154 | You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You. 155 | 156 | Our Service may give You the ability to delete certain information about You from within the Service. 157 | 158 | You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us. 159 | 160 | Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so. 161 | 162 | ### Disclosure of Your Personal Data 163 | 164 | #### Business Transactions 165 | 166 | If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy. 167 | 168 | #### Law enforcement 169 | 170 | Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency). 171 | 172 | #### Other legal requirements 173 | 174 | The Company may disclose Your Personal Data in the good faith belief that such action is necessary to: 175 | 176 | - Comply with a legal obligation 177 | - Protect and defend the rights or property of the Company 178 | - Prevent or investigate possible wrongdoing in connection with the Service 179 | - Protect the personal safety of Users of the Service or the public 180 | - Protect against legal liability 181 | 182 | ### Security of Your Personal Data 183 | 184 | The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security. 185 | 186 | ## Children's Privacy 187 | 188 | Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers. 189 | 190 | If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information. 191 | 192 | ## Links to Other Websites 193 | 194 | Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. 195 | 196 | We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. 197 | 198 | ## Changes to this Privacy Policy 199 | 200 | We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. 201 | 202 | We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy. 203 | 204 | You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. 205 | 206 | ## Contact Us 207 | 208 | If you have any questions about this Privacy Policy, You can contact us: 209 | 210 | - By email: weijunext@gmail.com 211 | -------------------------------------------------------------------------------- /contentlayer.config.js: -------------------------------------------------------------------------------- 1 | import { defineDocumentType, makeSource } from "contentlayer/source-files" 2 | import rehypeAutolinkHeadings from "rehype-autolink-headings" 3 | import rehypePrettyCode from "rehype-pretty-code" 4 | import rehypeSlug from "rehype-slug" 5 | import remarkGfm from "remark-gfm" 6 | 7 | /** @type {import('contentlayer/source-files').ComputedFields} */ 8 | const computedFields = { 9 | slug: { 10 | type: "string", 11 | resolve: (doc) => `/${doc._raw.flattenedPath}`, 12 | }, 13 | slugAsParams: { 14 | type: "string", 15 | resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"), 16 | }, 17 | } 18 | 19 | export const Post = defineDocumentType(() => ({ 20 | name: "Post", 21 | filePathPattern: `**/*.mdx`, 22 | contentType: "mdx", 23 | fields: { 24 | title: { 25 | type: "string", 26 | required: true, 27 | }, 28 | description: { 29 | type: "string", 30 | }, 31 | // date: { 32 | // type: "date", 33 | // required: true, 34 | // }, 35 | // published: { 36 | // type: "boolean", 37 | // default: true, 38 | // }, 39 | // image: { 40 | // type: "string", 41 | // required: true, 42 | // }, 43 | // authors: { 44 | // // Reference types are not embedded. 45 | // // Until this is fixed, we can use a simple list. 46 | // // type: "reference", 47 | // // of: Author, 48 | // type: "list", 49 | // of: { type: "string" }, 50 | // required: true, 51 | // }, 52 | }, 53 | computedFields, 54 | })) 55 | 56 | export default makeSource({ 57 | contentDirPath: "./content", 58 | documentTypes: [Post], 59 | mdx: { 60 | remarkPlugins: [remarkGfm], 61 | rehypePlugins: [ 62 | rehypeSlug, 63 | [ 64 | rehypePrettyCode, 65 | { 66 | theme: "github-dark", 67 | onVisitLine(node) { 68 | // Prevent lines from collapsing in `display: grid` mode, and allow empty 69 | // lines to be copy/pasted 70 | if (node.children.length === 0) { 71 | node.children = [{ type: "text", value: " " }] 72 | } 73 | }, 74 | onVisitHighlightedLine(node) { 75 | node.properties.className.push("line--highlighted") 76 | }, 77 | onVisitHighlightedWord(node) { 78 | node.properties.className = ["word--highlighted"] 79 | }, 80 | }, 81 | ], 82 | [ 83 | rehypeAutolinkHeadings, 84 | { 85 | properties: { 86 | className: ["subheading-anchor"], 87 | ariaLabel: "Link to section", 88 | }, 89 | }, 90 | ], 91 | ], 92 | }, 93 | }) 94 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | # how to use? see my blog: https://weijunext.com/article/b33a5545-fd26-47a6-8641-3c7467fb3910 3 | version: '3.1' 4 | services: 5 | db: 6 | image: postgres 7 | volumes: 8 | - ./postgres:/var/lib/postgresql/data 9 | restart: always 10 | ports: 11 | - 5432:5432 12 | environment: 13 | - POSTGRES_USER=myuser 14 | - POSTGRES_PASSWORD=mypassword 15 | 16 | adminer: 17 | image: adminer 18 | restart: always 19 | ports: 20 | - 8080:8080 21 | -------------------------------------------------------------------------------- /gtag.js: -------------------------------------------------------------------------------- 1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_ID 2 | 3 | export const pageview = url => { 4 | window.gtag("config", GA_TRACKING_ID, { 5 | page_path: url, 6 | }) 7 | } 8 | 9 | export const event = ({ action, category, label, value }) => { 10 | window.gtag("event", action, { 11 | event_category: category, 12 | event_label: label, 13 | value: value, 14 | }) 15 | } -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { ONE_DAY } from "@/lib/constants"; 2 | import { getUserSubscriptionStatus } from "@/lib/lemonsqueezy/subscriptionFromStorage"; 3 | import prisma from "@/lib/prisma"; 4 | import { UserInfo } from "@/types/user"; 5 | import { Account, NextAuthOptions, TokenSet } from "next-auth"; 6 | import { JWT } from "next-auth/jwt"; 7 | import GithubProvider from 'next-auth/providers/github'; 8 | import GoogleProvider from 'next-auth/providers/google'; 9 | import redis from "./redis"; 10 | 11 | // Here you define the type for the token object that includes accessToken. 12 | interface ExtendedToken extends TokenSet { 13 | accessToken?: string; 14 | userId?: string; 15 | } 16 | 17 | export const authOptions: NextAuthOptions = { 18 | secret: process.env.NEXTAUTH_SECRET, 19 | session: { 20 | strategy: "jwt", 21 | }, 22 | pages: { 23 | signIn: "/auth/login", 24 | signOut: '/auth/logout', 25 | }, 26 | providers: [ 27 | GithubProvider({ 28 | clientId: `${process.env.GITHUB_ID}`, 29 | clientSecret: `${process.env.GITHUB_SECRET}`, 30 | httpOptions: { 31 | timeout: 50000, 32 | }, 33 | }), 34 | GoogleProvider({ 35 | clientId: `${process.env.GOOGLE_ID}`, 36 | clientSecret: `${process.env.GOOGLE_SECRET}` 37 | }), 38 | ], 39 | callbacks: { 40 | async jwt({ token, account }) { 41 | // 登录(account仅登录那一次有值) 42 | // Only on sign in (account only has a value at that time) 43 | if (account) { 44 | token.accessToken = account.access_token 45 | 46 | // 存储访问令牌 47 | // Store the access token 48 | await storeAccessToken(account.access_token || '', token.sub); 49 | 50 | // 用户信息存入数据库 51 | // Save user information in the database 52 | const userInfo = await upsertUserAndGetInfo(token, account); 53 | if (!userInfo || !userInfo.userId) { 54 | throw new Error('User information could not be saved or retrieved.'); 55 | } 56 | 57 | const planRes = await getUserSubscriptionStatus({ userId: userInfo.userId, defaultUser: userInfo }) 58 | const fullUserInfo = { 59 | userId: userInfo.userId, 60 | username: userInfo.username, 61 | avatar: userInfo.avatar, 62 | email: userInfo.email, 63 | platform: userInfo.platform, 64 | role: planRes.role, 65 | membershipExpire: planRes.membershipExpire, 66 | accessToken: account.access_token 67 | } 68 | return fullUserInfo 69 | } 70 | return token as any 71 | }, 72 | async session({ session, token }) { 73 | // Append user information to the session 74 | if (token && token.userId) { 75 | session.user = await getSessionUser(token); 76 | } 77 | return session; 78 | } 79 | }, 80 | } 81 | async function storeAccessToken(accessToken: string, sub?: string) { 82 | if (!accessToken || !sub) return; 83 | const expire = ONE_DAY * 30; // The number of seconds in 30 days 84 | await redis.set(accessToken, sub, { ex: expire }); 85 | } 86 | async function upsertUserAndGetInfo(token: JWT, account: Account) { 87 | const user = await upsertUser(token, account.provider); 88 | if (!user || !user.userId) return null; 89 | 90 | const subscriptionStatus = await getUserSubscriptionStatus({ userId: user.userId, defaultUser: user }); 91 | 92 | return { 93 | ...user, 94 | role: subscriptionStatus.role, 95 | membershipExpire: subscriptionStatus.membershipExpire, 96 | }; 97 | } 98 | async function upsertUser(token: JWT, provider: string) { 99 | const userData = { 100 | userId: token.sub, 101 | username: token.name, 102 | avatar: token.picture, 103 | email: token.email, 104 | platform: provider, 105 | }; 106 | 107 | const user = await prisma.user.upsert({ 108 | where: { userId: token.sub }, 109 | update: userData, 110 | create: { ...userData, role: 0 }, 111 | }); 112 | 113 | return user || null; 114 | } 115 | async function getSessionUser(token: ExtendedToken): Promise { 116 | const planRes = await getUserSubscriptionStatus({ userId: token.userId as string }); 117 | return { 118 | userId: token.userId, 119 | username: token.username, 120 | avatar: token.avatar, 121 | email: token.email, 122 | platform: token.platform, 123 | role: planRes.role, 124 | membershipExpire: planRes.membershipExpire, 125 | accessToken: token.accessToken 126 | } as UserInfo; 127 | } 128 | -------------------------------------------------------------------------------- /lib/axios.ts: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | 3 | export const axios = Axios.create({ 4 | baseURL: "/", 5 | }); 6 | 7 | axios.interceptors.response.use( 8 | (response) => response.data, 9 | (error) => { 10 | const message = error.response?.data?.message || error.message; 11 | // toast.error(message); 12 | console.log(message); 13 | return Promise.reject(error); 14 | } 15 | ); -------------------------------------------------------------------------------- /lib/clsxm.ts: -------------------------------------------------------------------------------- 1 | import clsx, { ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** Merge classes with tailwind-merge with clsx full feature */ 5 | export default function clsxm(...classes: ClassValue[]) { 6 | return twMerge(clsx(...classes)); 7 | } 8 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 0 普通用户; 2 会员 3 | * Role values: 0 for Basic User; 2 for Member 4 | */ 5 | 6 | import { VariantIdsByType } from "@/types/subscribe"; 7 | import { Role, UserId } from "@/types/user"; 8 | 9 | // Definitions for user roles. 10 | export const ROLES: { [key in Role]: string } = { 11 | 0: 'Basic', 12 | 2: 'MemberShip', 13 | } 14 | 15 | // Daily usage limits for different roles. 16 | export const ROLES_LIMIT: { [key in Role]: number } = { 17 | 0: process.env.NEXT_PUBLIC_COMMON_USER_DAILY_LIMIT_STR && Number(process.env.NEXT_PUBLIC_COMMON_USER_DAILY_LIMIT_STR) || 10, 18 | 2: process.env.NEXT_PUBLIC_MEMBERSHIP_DAILY_LIMIT_STR && Number(process.env.NEXT_PUBLIC_MEMBERSHIP_DAILY_LIMIT_STR) || 500, 19 | } 20 | 21 | 22 | export const ONE_DAY = 3600 * 24 23 | export const DATE_USAGE_KEY_EXPIRE = 3600 * 24 * 10 // 10天,用户日用量保存时长 10 days, duration for saving daily user usage data 24 | export const MEMBERSHIP_ROLE_VALUE = 2 // 月度会员的值 The value for monthly membership 25 | export const BOOST_PACK_EXPIRE = ONE_DAY * Number(process.env.NEXT_PUBLIC_BOOST_PACK_EXPIRE_DAYS || 7) // 7天,购买加油包的使用期限 7 days, usage duration for a purchased boost pack 26 | export const BOOST_PACK_CREDITS = Number(process.env.NEXT_PUBLIC_BOOST_PACK_CREDITS || 100) // 每次购买加油包获得的次数 Number of uses received per boost pack purchase 27 | 28 | // Functions to create cache keys for tracking user data. 29 | export const getUserDateUsageKey = ({ userId }: UserId) => { 30 | const currentDate = new Date().toLocaleDateString(); 31 | return `uid:${userId}::date:${currentDate}::user_date_usage` 32 | } 33 | export const getUserTotalUsageKey = ({ userId }: UserId) => { 34 | const key = `USER_USAGE::uid:${userId}`; 35 | return key 36 | } 37 | export const getBoostPackKey = ({ userId }: UserId) => { 38 | return `uid:${userId}::boost_pack_balance` 39 | } 40 | 41 | // Variant keys for subscription types. 42 | export const SUBSCRIPTION_VARIANT_KEY = 'subscription' 43 | export const SINGLE_VARIANT_KEY = 'single' 44 | // Variant IDs for different subscription types, to be used in checkouts and webhooks. 45 | export const VARIANT_IDS_BY_TYPE: VariantIdsByType = { 46 | 'subscription': process.env.LEMON_SQUEEZY_MEMBERSHIP_MONTHLY_VARIANT_ID || '', // checkouts 请求传参要用string,但是webhook收到的variant_id是number 47 | 'single': process.env.LEMON_SQUEEZY_MEMBERSHIP_SINGLE_TIME_VARIANT_ID || '', 48 | } 49 | // Function to generate a cache key for single payment orders. 50 | export const getSinglePayOrderKey = ({ identifier }: { identifier: string }) => { 51 | return `single_${identifier}` 52 | } -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a number according to specified rules, such as adding a comma every three digits. 3 | * 4 | * @example 5 | * formatNumber({ 6 | * apart: 3, 7 | * separator: ',', 8 | * value: 123456789 9 | * }) // returns "123,456,789" 10 | * 11 | * @param {number} [apart=3] - Number of digits between separators 12 | * @param {string} [separator=','] - Character to be used as a separator 13 | * @param {number|string} value - The number or numeric string to format 14 | * @returns {string|null} - The formatted string or null if input is invalid 15 | */ 16 | interface FormatNumberType { 17 | apart?: number; 18 | separator?: string; 19 | value: number | string; 20 | } 21 | 22 | export const formatNumber = ({ 23 | apart = 3, 24 | separator = ',', 25 | value 26 | }: FormatNumberType): string | null => { 27 | const stringValue = String(value).trim(); 28 | 29 | // Verify if the value is a number and is not empty 30 | if (isNaN(Number(value)) || stringValue === '') { 31 | console.error('An invalid value was passed to formatNumber'); 32 | return null; // Return null to indicate an error 33 | } 34 | 35 | const regex = new RegExp(`(\\d)(?=(\\d{${apart}})+(?!\\d))`, 'g'); 36 | const [integerPart, decimalPart] = stringValue.split('.'); 37 | 38 | // Format the integer part 39 | const formattedIntegerPart = integerPart.replace(regex, `$1${separator}`); 40 | 41 | // Concatenate the integer part with the decimal part if it exists 42 | return decimalPart ? `${formattedIntegerPart}.${decimalPart}` : formattedIntegerPart; 43 | } 44 | -------------------------------------------------------------------------------- /lib/lemonsqueezy/lemons.ts: -------------------------------------------------------------------------------- 1 | import { LemonsqueezyClient } from "lemonsqueezy.ts"; 2 | 3 | export const client = new LemonsqueezyClient(process.env.LEMON_SQUEEZY_API_KEY as string); -------------------------------------------------------------------------------- /lib/lemonsqueezy/subscription.ts: -------------------------------------------------------------------------------- 1 | import { MEMBERSHIP_ROLE_VALUE } from "@/lib/constants"; 2 | import prisma from "@/lib/prisma"; 3 | import { LemonsqueezySubscriptionURLPatch, SubScriptionInfo } from "@/types/subscribe"; 4 | import { UserId } from "@/types/user"; 5 | import { client } from "./lemons"; 6 | 7 | export async function getUserSubscriptionPlan({ userId }: UserId) { 8 | const user = await prisma.user.findUnique({ 9 | where: { userId }, 10 | select: { 11 | subscriptionId: true, 12 | currentPeriodEnd: true, 13 | customerId: true, 14 | variantId: true, 15 | }, 16 | }); 17 | 18 | if (!user) throw new Error("User not found"); 19 | if (!user.subscriptionId) return null 20 | 21 | const membershipExpire = (user.currentPeriodEnd || 0) * 1000 22 | const subscription = await client.retrieveSubscription({ id: user.subscriptionId }); 23 | 24 | const attributes = subscription.data.attributes 25 | const urls = attributes.urls as LemonsqueezySubscriptionURLPatch 26 | 27 | // Check if user is on a pro plan. 28 | const isMembership = 29 | user.variantId && 30 | membershipExpire > Date.now().valueOf(); 31 | 32 | // If user has a pro plan, check cancel status on Stripe. 33 | let isCanceled = false; 34 | if (isMembership && user.subscriptionId) { 35 | isCanceled = attributes.cancelled; 36 | } 37 | 38 | return { 39 | subscriptionId: user.subscriptionId, 40 | membershipExpire: isMembership ? membershipExpire : 0, 41 | customerId: user.customerId, 42 | variantId: user.variantId, 43 | role: isMembership ? MEMBERSHIP_ROLE_VALUE : 0, // 2 : 0 44 | isCanceled, 45 | updatePaymentMethodURL: urls.update_payment_method, 46 | customerPortal: urls.customer_portal, 47 | } as SubScriptionInfo; 48 | } -------------------------------------------------------------------------------- /lib/lemonsqueezy/subscriptionFromStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve the user's role and membership expiration date from the database. 3 | */ 4 | import { MEMBERSHIP_ROLE_VALUE } from "@/lib/constants"; 5 | import prisma from "@/lib/prisma"; 6 | import { SubScriptionInfo } from "@/types/subscribe"; 7 | import { PrismaUser } from "@/types/user"; 8 | 9 | export async function getUserSubscriptionStatus({ userId, defaultUser }: { userId: string; defaultUser?: PrismaUser }) { 10 | let user = null 11 | if (defaultUser) { 12 | user = defaultUser 13 | } else { 14 | user = await prisma.user.findUnique({ 15 | where: { userId }, 16 | select: { 17 | subscriptionId: true, 18 | currentPeriodEnd: true, 19 | customerId: true, 20 | variantId: true, 21 | }, 22 | }); 23 | } 24 | 25 | if (!user) throw new Error("User not found"); 26 | 27 | const membershipExpire = (user.currentPeriodEnd || 0) * 1000 // 13-digit timestamp or non-member 28 | const isMembership = 29 | user.variantId && 30 | membershipExpire > Date.now().valueOf(); 31 | 32 | return { 33 | subscriptionId: user.subscriptionId, 34 | membershipExpire: isMembership ? membershipExpire : 0, 35 | customerId: user.customerId, 36 | variantId: user.variantId, 37 | role: isMembership ? MEMBERSHIP_ROLE_VALUE : 0, // 2 : 0 38 | } as SubScriptionInfo; 39 | } -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-var 5 | var prisma: PrismaClient 6 | } 7 | 8 | let prisma: PrismaClient; 9 | 10 | if (process.env.NODE_ENV === "production") { 11 | prisma = new PrismaClient(); 12 | } else { 13 | if (!global.prisma) { 14 | global.prisma = new PrismaClient(); 15 | } 16 | prisma = global.prisma; 17 | } 18 | export default prisma; 19 | -------------------------------------------------------------------------------- /lib/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from '@upstash/redis' 2 | 3 | const redis = new Redis({ 4 | url: `${process.env.UPSTASH_REDIS_REST_URL}`, 5 | token: `${process.env.UPSTASH_REDIS_REST_TOKEN}`, 6 | retry: { 7 | retries: 5, 8 | backoff: (retryCount) => Math.exp(retryCount) * 50, 9 | }, 10 | }) 11 | 12 | export default redis -------------------------------------------------------------------------------- /lib/response/responseUtils.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export function unauthorizedResponse(message: string) { 4 | return NextResponse.json({ error: message }, { status: 401 }); 5 | } -------------------------------------------------------------------------------- /lib/session.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next"; 2 | 3 | import { authOptions } from "@/lib/auth"; 4 | 5 | export async function getCurrentUser() { 6 | const session = await getServerSession(authOptions) 7 | 8 | return session?.user 9 | } 10 | -------------------------------------------------------------------------------- /lib/upgrade/upgrade.ts: -------------------------------------------------------------------------------- 1 | import { BOOST_PACK_CREDITS, BOOST_PACK_EXPIRE, getBoostPackKey } from "@/lib/constants"; 2 | import redis from "@/lib/redis"; 3 | import { UserId } from "@/types/user"; 4 | import 'server-only'; 5 | 6 | /** 7 | * 设计:购买加油包 8 | * 如果已有加油包(expire存在且大于当前时间),expire + 7天,oldBalance + BOOST_PACK_CREDITS 9 | * 如果没有加油包,设置expire为 0 + 7天,0 + BOOST_PACK_CREDITS 10 | * 11 | * Logic: Purchase of boost pack 12 | * If a boost pack already exists (expire exists and is greater than the current time), extend expire by 7 days, and add BOOST_PACK_CREDITS to oldBalance 13 | * If no boost pack exists, set expire to current time + 7 days, and set balance to BOOST_PACK_CREDITS 14 | */ 15 | export const boostPack = async ({ userId }: UserId) => { 16 | const userBoostPackKey = await getBoostPackKey({ userId }) 17 | const userBoostPack = await redis.get(userBoostPackKey) || 0 18 | if (userBoostPack === 0) { 19 | const res = await redis.set(userBoostPackKey, BOOST_PACK_CREDITS, { ex: BOOST_PACK_EXPIRE }) 20 | if (res === 'OK') { 21 | return { userId, balance: BOOST_PACK_CREDITS, expire: BOOST_PACK_EXPIRE, boostPack: 'success' } 22 | } 23 | return { userId, balance: 0, expire: 0, boostPack: 'fail' } 24 | } 25 | // 已是加油包用户,查询过期时间,计算新的过期时间,更新过期时间 26 | // For existing boost pack users, query the expiration time, calculate the new expiration time, and update the expiration time. 27 | const oldBalance: number = await redis.get(userBoostPackKey) || 0 28 | const TTL = await redis.ttl(userBoostPackKey) 29 | const newTTL = TTL + BOOST_PACK_EXPIRE 30 | const newBalance = oldBalance + BOOST_PACK_CREDITS 31 | const res = await redis.setex(userBoostPackKey, newTTL, newBalance) 32 | return res === 'OK' ? 33 | { userId, oldBalance, newBalance, expire: newTTL, boostPack: 'success' } : 34 | { userId, oldBalance, newBalance: oldBalance, expire: TTL, boostPack: 'fail' } 35 | } -------------------------------------------------------------------------------- /lib/usage/usage.ts: -------------------------------------------------------------------------------- 1 | import { DATE_USAGE_KEY_EXPIRE, ROLES_LIMIT, getBoostPackKey, getUserDateUsageKey, getUserTotalUsageKey } from "@/lib/constants"; 2 | import { getUserSubscriptionStatus } from "@/lib/lemonsqueezy/subscriptionFromStorage"; 3 | import redis from "@/lib/redis"; 4 | import { DateRemaining } from "@/types/usage"; 5 | import { RemainingParams, Role, UserId } from "@/types/user"; 6 | import 'server-only'; 7 | 8 | interface IncrAfterChat { 9 | userId: string; 10 | remainingInfo: DateRemaining; 11 | } 12 | export const incrAfterChat = async ({ userId, remainingInfo }: IncrAfterChat) => { 13 | // 网站总使用用量自增 14 | // Increment website total usage after chat 15 | incrUsage() 16 | 17 | /** 18 | * 用户使用量自增 19 | * Increment user usage 20 | */ 21 | 22 | // 如果有默认次数,增加一次日使用次数 23 | // If there is a default number of times, add one to the daily usage count 24 | if (remainingInfo.userTodayRemaining > 0) { 25 | await incrementDailyUserUsage({ userId }) 26 | return 27 | } 28 | // 如果没有默认次数,有加油包,扣除加油包次数 29 | // If there is no default number but a boost pack is available, deduct from the boost pack count 30 | if (remainingInfo.boostPackRemaining > 0) { 31 | const boostPackKey = await getBoostPackKey({ userId }) 32 | await redis.decr(boostPackKey) 33 | return 34 | } 35 | // 如果没有默认次数,也没有加油包,则不处理 36 | // If there is no default number or boost pack, do nothing 37 | console.log('0 credit remaining today.'); 38 | } 39 | 40 | // 自增网站总使用次数 41 | // Increment total website usage count 42 | export const incrUsage = async () => { 43 | await redis.incr('usage'); 44 | } 45 | // 获取网站总使用次数 46 | // Get total website usage count 47 | export const getUsage = async () => { 48 | const usage = await redis.get('usage') || 0; 49 | return usage 50 | } 51 | 52 | // 自增用户使用次数,这个方法用于内部调用 53 | // Increment user's daily usage count, this method is for internal use 54 | export const incrementDailyUserUsage = async ({ userId }: UserId) => { 55 | const keyDate = getUserDateUsageKey({ userId }); 56 | const keyTotal = getUserTotalUsageKey({ userId }) 57 | 58 | // 使用 Redis pipeline 59 | // Use Redis pipeline 60 | const pipeline = redis.pipeline(); 61 | // 将所有命令加入 pipeline 62 | // Add all commands to the pipeline 63 | pipeline.incr(keyDate); // 增加日使用量 Increase daily usage 64 | pipeline.incr(keyTotal); // 增加总使用量 Increase total usage 65 | pipeline.expire(keyDate, DATE_USAGE_KEY_EXPIRE); // 设置日使用量的过期时间 // Set expiration time for daily usage 66 | 67 | try { 68 | await pipeline.exec(); 69 | } catch (error) { 70 | console.error('An error occurred while incrementing user usage data:', error); 71 | } 72 | } 73 | 74 | // 获取用户日使用次数 75 | // Get User's daily usage count 76 | export const getUserDateUsage = async ({ userId }: UserId) => { 77 | const keyDate = getUserDateUsageKey({ userId }); 78 | const userTodayUsageStr = await redis.get(keyDate) as string | null; 79 | const userTodayUsage = parseInt(userTodayUsageStr ?? '0', 10); 80 | return { userTodayUsage }; 81 | } 82 | // 获取用户总使用次数 83 | // Get user's total usage count 84 | export const getUserTotalUsage = async ({ userId }: UserId) => { 85 | const keyTotal = `USER_USAGE::uid:${userId}`; 86 | const userTotalUsageStr = await redis.get(keyTotal) as string | null; 87 | const userTotalUsage = parseInt(userTotalUsageStr ?? '0', 10); 88 | return { 89 | userTotalUsage 90 | } 91 | } 92 | 93 | // 计算当日可用次数:查询当日已用次数,计算剩余次数,再加上加油包剩余次数 94 | // Calculate the available number of times for the day: Query the number of times used that day, calculate the remaining number, and then add the remaining number of boost packs 95 | export const getUserDateRemaining = async ({ userId, role }: RemainingParams) => { 96 | const { userTodayUsage } = await getUserDateUsage({ userId }) 97 | 98 | let userRole: Role = 0 99 | if (role) { 100 | // 有传角色,复用订阅信息 101 | // If the role is passed, reuse subscription information 102 | userRole = role 103 | } else { 104 | // 没传角色,重新请求订阅信息 105 | // If no role is passed, request subscription information again 106 | const subscriptionRes = await getUserSubscriptionStatus({ 107 | userId, 108 | }) 109 | userRole = subscriptionRes.role 110 | } 111 | 112 | const userDateDefaultLimit: number = ROLES_LIMIT[userRole] 113 | 114 | const userTodayRemaining = userDateDefaultLimit - userTodayUsage <= 0 ? 0 : userDateDefaultLimit - userTodayUsage 115 | const boostPackKey = await getBoostPackKey({ userId }) 116 | const boostPackRemaining: number = await redis.get(boostPackKey) || 0 117 | // 查询次数是在请求openai前,自增次数是在请求后,这里把查询到的redis剩余次数返回,并传给自增方法,减少redis请求次数 118 | // The query for the number of times is before the request to openai, and the increment of the number of times is after the request, here the remaining redis times queried are returned and passed to the increment method, reducing the number of redis requests 119 | return { 120 | userTodayRemaining, 121 | boostPackRemaining, 122 | userDateRemaining: userTodayRemaining + boostPackRemaining 123 | } 124 | } 125 | // 计算当日剩余次数、会员到期时间、加油包剩余次数、加油包到期时间 126 | // Calculate today's remaining count, membership expiration time, remaining boost pack count, and boost pack expiration time 127 | export const checkStatus = async ({ userId }: UserId) => { 128 | // 获取用户订阅信息(角色、会员到期时间戳) 129 | // Get user subscription information 130 | const subscriptionRes = await getUserSubscriptionStatus({ 131 | userId, 132 | }); 133 | 134 | const pipeline = redis.pipeline(); 135 | const keyDate = getUserDateUsageKey({ userId }); 136 | pipeline.get(keyDate); 137 | const boostPackKey = getBoostPackKey({ userId }); 138 | pipeline.get(boostPackKey); 139 | 140 | const pipelineResults = await pipeline.exec(); 141 | const userTodayUsageStr = pipelineResults[0] as string; 142 | const boostPackRemainingStr = pipelineResults[1] as string; 143 | 144 | const userTodayUsage = parseInt(userTodayUsageStr ?? '0', 10); 145 | const boostPackRemaining = parseInt(boostPackRemainingStr ?? '0', 10); 146 | 147 | const userDateDefaultLimit: number = ROLES_LIMIT[subscriptionRes.role]; 148 | const userTodayRemaining = Math.max(userDateDefaultLimit - userTodayUsage, 0); 149 | 150 | // 获取加油包到期时间,如果有剩余次数 151 | // remaining boost pack count, and boost pack expiration time. 152 | let boostPackExpire = 0 153 | if (boostPackRemaining > 0) { 154 | boostPackExpire = await redis.ttl(boostPackKey) 155 | } 156 | 157 | return { 158 | role: subscriptionRes.role, 159 | todayRemaining: userTodayRemaining, 160 | membershipExpire: subscriptionRes.membershipExpire, 161 | boostPackRemaining, 162 | boostPackExpire, 163 | }; 164 | }; 165 | 166 | // 升级后清空当日已用次数 167 | // Clear the day's used count after upgrade. 168 | export const clearTodayUsage = async ({ userId }: UserId) => { 169 | const userDateUsageKey = getUserDateUsageKey({ userId }) 170 | await redis.setex(userDateUsageKey, DATE_USAGE_KEY_EXPIRE, 0) 171 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /lib/verifyUtils/verifyUtils.ts: -------------------------------------------------------------------------------- 1 | import redis from "@/lib/redis"; 2 | 3 | export async function verifyReferer(request: Request) { 4 | const referer = request.headers.get('referer'); 5 | if (!referer || !referer.includes(process.env.REFERER_MAIN_URL as string)) { 6 | return false; 7 | } 8 | return true; 9 | } 10 | 11 | export async function verifyToken(request: Request) { 12 | const token = request.headers.get('token'); 13 | if (!token) { 14 | return false; 15 | } 16 | const userId = await redis.get(token) + ''; 17 | if (!userId) { 18 | return false; 19 | } 20 | return userId; 21 | } -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('next-sitemap').IConfig} 3 | * @see https://github.com/iamvishnusankar/next-sitemap#readme 4 | * @see https://weijunext.com/article/979b9033-188c-4d88-bfff-6cf74d28420d 5 | */ 6 | module.exports = { 7 | siteUrl: "https://www.smartexcel.cc", 8 | changefreq: "daily", 9 | priority: 0.7, 10 | exclude: ["/server-sitemap.xml", "/404"], 11 | generateRobotsTxt: true, 12 | sitemapSize: 5000, // 站点超过5000个,拆分到多个文件 13 | transform: async (config, path) => { 14 | return { 15 | loc: path, // => this will be exported as http(s):// / 16 | changefreq: config.changefreq, 17 | priority: config.priority, 18 | lastmod: config.autoLastmod ? new Date().toISOString() : undefined, 19 | alternateRefs: config.alternateRefs ?? [], 20 | }; 21 | }, 22 | additionalPaths: async (config) => [ 23 | // 这个版本的next-sitemap无法把app router的静态目录加载进来,所以在这里手动添加了 24 | await config.transform(config, "/"), 25 | ], 26 | robotsTxtOptions: { 27 | // additionalSitemaps: [ 28 | // 'https://www.smartexcel.cc/sitemap.xml', 29 | // ], 30 | policies: [ 31 | { 32 | userAgent: "*", 33 | allow: "/", 34 | }, 35 | { 36 | userAgent: "AhrefsBot", 37 | disallow: ["/"], 38 | }, 39 | { 40 | userAgent: "SemrushBot", 41 | disallow: ["/"], 42 | }, 43 | { 44 | userAgent: "MJ12bot", 45 | disallow: ["/"], 46 | }, 47 | { 48 | userAgent: "DotBot", 49 | disallow: ["/"], 50 | }, 51 | ], 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // import { withContentlayer } from "next-contentlayer"; 2 | const { withContentlayer } = require("next-contentlayer"); 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: process.env.NODE_ENV === "development", 7 | swcMinify: true, 8 | images: { 9 | domains: [ 10 | "avatars.githubusercontent.com", 11 | "weijunext.com", 12 | "smartexcel.cc", 13 | ], 14 | }, 15 | async redirects() { 16 | return [ 17 | { 18 | source: "/github", 19 | destination: "https://github.com/weijunext/smart-excel-ai", 20 | permanent: false, 21 | }, 22 | ]; 23 | }, 24 | }; 25 | 26 | // export default withContentlayer(nextConfig) 27 | module.exports = withContentlayer(nextConfig); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "dev:mdx": "concurrently \"contentlayer dev\" \"next dev\"", 6 | "build:local": "contentlayer build && next build", 7 | "build": "contentlayer build && prisma generate && prisma db push && next build", 8 | "postbuild": "next-sitemap --config next-sitemap.config.js", 9 | "start": "next start", 10 | "prisma:init": "npx prisma init", 11 | "prisma:generate": "npx prisma generate", 12 | "prisma:migrate:init": "npx prisma migrate dev --name init", 13 | "docker:up": "docker-compose up", 14 | "docker:down": "docker-compose down" 15 | }, 16 | "dependencies": { 17 | "@headlessui/react": "^1.7.7", 18 | "@headlessui/tailwindcss": "^0.1.2", 19 | "@heroicons/react": "^2.0.13", 20 | "@next-auth/prisma-adapter": "^1.0.7", 21 | "@prisma/client": "^5.2.0", 22 | "@radix-ui/react-alert-dialog": "^1.0.5", 23 | "@radix-ui/react-avatar": "^1.0.3", 24 | "@radix-ui/react-dropdown-menu": "^2.0.5", 25 | "@radix-ui/react-slot": "^1.0.2", 26 | "@tailwindcss/forms": "^0.5.6", 27 | "@types/mdx": "^2.0.6", 28 | "@upstash/redis": "^1.22.0", 29 | "@vercel/analytics": "^0.1.8", 30 | "ai": "^2.1.21", 31 | "axios": "^1.4.0", 32 | "class-variance-authority": "^0.7.0", 33 | "clsx": "^2.0.0", 34 | "concurrently": "^8.2.0", 35 | "contentlayer": "^0.3.4", 36 | "dayjs": "^1.11.10", 37 | "ioredis": "^5.3.2", 38 | "lemonsqueezy.ts": "^0.1.7", 39 | "logging-service": "^1.0.1", 40 | "lucide-react": "^0.264.0", 41 | "next": "^13.4.10", 42 | "next-auth": "^4.22.3", 43 | "next-contentlayer": "^0.3.4", 44 | "next-mdx-remote": "^4.4.1", 45 | "next-sitemap": "^4.2.3", 46 | "next-themes": "^0.2.1", 47 | "openai": "^4.4.0", 48 | "openai-edge": "^1.2.0", 49 | "prisma": "^5.4.2", 50 | "raw-body": "^2.5.2", 51 | "react": "18.2.0", 52 | "react-dom": "18.2.0", 53 | "react-hook-form": "^7.42.0", 54 | "react-hot-toast": "^2.4.1", 55 | "react-icons": "^5.0.1", 56 | "react-use-measure": "^2.1.1", 57 | "rehype-autolink-headings": "^6.1.1", 58 | "rehype-pretty-code": "^0.10.0", 59 | "rehype-raw": "^6.1.1", 60 | "rehype-slug": "^5.1.0", 61 | "rehype-stringify": "^9.0.3", 62 | "remark-gfm": "^3.0.1", 63 | "remark-math": "^5.1.1", 64 | "remark-rehype": "^10.1.0", 65 | "server-only": "^0.0.1", 66 | "tailwind-merge": "^1.14.0", 67 | "tailwindcss-animate": "^1.0.7", 68 | "use-debounce": "^9.0.4" 69 | }, 70 | "devDependencies": { 71 | "@tailwindcss/line-clamp": "^0.4.4", 72 | "@tailwindcss/typography": "^0.5.10", 73 | "@types/node": "18.11.3", 74 | "@types/react": "18.0.21", 75 | "@types/react-dom": "18.0.6", 76 | "autoprefixer": "^10.4.12", 77 | "postcss": "^8.4.18", 78 | "postcss-import": "^15.1.0", 79 | "tailwindcss": "^3.3.3", 80 | "typescript": "4.9.4" 81 | } 82 | } -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | 3 | import { authOptions } from "@/lib/auth" 4 | 5 | // @see ./lib/auth 6 | export default NextAuth(authOptions) 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schem 3 | 4 | // generator:指定哪个客户端向数据发送查询语言 5 | generator client { 6 | provider = "prisma-client-js" 7 | } 8 | 9 | // datasource:定义数据库类型和链接地址 10 | datasource db { 11 | provider = "postgresql" 12 | url = env("POSTGRES_PRISMA_URL") 13 | directUrl = env("POSTGRES_URL_NON_POOLING") 14 | } 15 | 16 | // model:定义数据库 Schema。 17 | model Dialog { 18 | id Int @id @default(autoincrement()) 19 | dialogId String @unique 20 | prompt String 21 | reply String 22 | userId String 23 | uid User @relation(fields: [userId], references: [userId]) 24 | createdAt DateTime @default(now()) 25 | updatedAt DateTime @updatedAt 26 | } 27 | 28 | model User { 29 | id Int @id @default(autoincrement()) 30 | userId String? @unique // sub 31 | username String? 32 | avatar String? 33 | role Int? 34 | platform String? // github google 35 | email String? 36 | // lemonsqueezy 37 | subscriptionId String? 38 | customerId String? 39 | variantId Int? 40 | currentPeriodEnd Int? 41 | 42 | createdAt DateTime @default(now()) 43 | updatedAt DateTime @updatedAt 44 | Dialog Dialog[] 45 | } 46 | -------------------------------------------------------------------------------- /public/1-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/1-black.png -------------------------------------------------------------------------------- /public/2-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/2-black.png -------------------------------------------------------------------------------- /public/302banner1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/302banner1.jpg -------------------------------------------------------------------------------- /public/afd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/afd.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/nexty_og.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/nexty_og.webp -------------------------------------------------------------------------------- /public/nexty_og_zh.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/nexty_og_zh.webp -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/og.png -------------------------------------------------------------------------------- /public/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/opengraph-image.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # AhrefsBot 6 | User-agent: AhrefsBot 7 | Disallow: / 8 | 9 | # SemrushBot 10 | User-agent: SemrushBot 11 | Disallow: / 12 | 13 | # MJ12bot 14 | User-agent: MJ12bot 15 | Disallow: / 16 | 17 | # DotBot 18 | User-agent: DotBot 19 | Disallow: / 20 | 21 | # Host 22 | Host: https://www.smartexcel.cc 23 | 24 | # Sitemaps 25 | Sitemap: https://www.smartexcel.cc/sitemap.xml 26 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/screenshot.png -------------------------------------------------------------------------------- /public/sitemap-0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 |4 | https://www.smartexcel.cc 2024-08-12T06:13:41.519Z daily 0.7 3 | -------------------------------------------------------------------------------- /public/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/twitter-image.png -------------------------------------------------------------------------------- /public/zs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weijunext/smart-excel-ai/13100100ab21084133ce417bd8167096b476fd90/public/zs.jpeg -------------------------------------------------------------------------------- /sitemap.ts: -------------------------------------------------------------------------------- 1 | import fs, { Dirent } from 'fs'; 2 | import { MetadataRoute } from 'next'; 3 | import path from 'path'; 4 | 5 | export function getFoldersRecursive(filePath: string): string[] { 6 | const folders: string[] = []; 7 | 8 | function shouldIgnoreFolder(folderName: string): boolean { 9 | const ignoredPrefixes = ['[', '(', '_', '-', 'api']; 10 | return ignoredPrefixes.some((prefix) => folderName.startsWith(prefix)); 11 | } 12 | 13 | function traverse(currentPath: string): void { 14 | const files = fs.readdirSync(currentPath, { withFileTypes: true }); 15 | 16 | files.forEach((file: Dirent) => { 17 | if (file.isDirectory()) { 18 | const folderName = file.name; 19 | if (!shouldIgnoreFolder(folderName)) { 20 | folders.push(folderName); 21 | traverse(path.join(currentPath, folderName)); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | traverse(filePath); 28 | return folders; 29 | } 30 | 31 | // Usage example 32 | const targetPath = '/app'; 33 | const folderNames = getFoldersRecursive(targetPath); 34 | 35 | 36 | export default function sitemap(): MetadataRoute.Sitemap { 37 | return folderNames as unknown as MetadataRoute.Sitemap; 38 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /styles/loading.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | @keyframes blink { 31 | 0% { 32 | opacity: 0.2; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 100% { 38 | opacity: 0.2; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /styles/mdx.css: -------------------------------------------------------------------------------- 1 | [data-rehype-pretty-code-fragment] code { 2 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black; 3 | counter-reset: line; 4 | box-decoration-break: clone; 5 | } 6 | [data-rehype-pretty-code-fragment] .line { 7 | @apply px-4 py-1; 8 | } 9 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { 10 | counter-increment: line; 11 | content: counter(line); 12 | display: inline-block; 13 | width: 1rem; 14 | margin-right: 1rem; 15 | text-align: right; 16 | color: gray; 17 | } 18 | [data-rehype-pretty-code-fragment] .line--highlighted { 19 | @apply bg-slate-300 bg-opacity-10; 20 | } 21 | [data-rehype-pretty-code-fragment] .line-highlighted span { 22 | @apply relative; 23 | } 24 | [data-rehype-pretty-code-fragment] .word--highlighted { 25 | @apply rounded-md bg-slate-300 bg-opacity-10 p-1; 26 | } 27 | [data-rehype-pretty-code-title] { 28 | @apply mt-4 py-2 px-4 text-sm font-medium; 29 | } 30 | [data-rehype-pretty-code-title] + pre { 31 | @apply mt-0; 32 | } 33 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const { fontFamily } = require("tailwindcss/defaultTheme") 3 | module.exports = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: 0 }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: 0 }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | fontFamily: { 75 | sans: ["var(--font-sans)", ...fontFamily.sans], 76 | }, 77 | }, 78 | }, 79 | plugins: [require("tailwindcss-animate")], 80 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./*"], 12 | "contentlayer/generated": ["./.contentlayer/generated"] 13 | }, 14 | "allowJs": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noEmit": true, 19 | "esModuleInterop": true, 20 | "module": "esnext", 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "jsx": "preserve", 25 | "incremental": true, 26 | "plugins": [ 27 | { 28 | "name": "next" 29 | } 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts", 37 | ".contentlayer/generated" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /types/layout.ts: -------------------------------------------------------------------------------- 1 | import { UserInfo } from "@/types/user"; 2 | 3 | export interface HomeLayoutChildren { 4 | children?: React.ReactNode; 5 | } 6 | export interface HomeLayoutProps extends HomeLayoutChildren { 7 | user?: UserInfo; 8 | } -------------------------------------------------------------------------------- /types/request.ts: -------------------------------------------------------------------------------- 1 | export interface Res { 2 | message: string; 3 | code: number; 4 | } -------------------------------------------------------------------------------- /types/sideNav.ts: -------------------------------------------------------------------------------- 1 | import { Icons } from "@/components/Icons" 2 | 3 | export type NavItem = { 4 | title: string 5 | href: string 6 | disabled?: boolean 7 | } 8 | 9 | export type MainNavItem = NavItem 10 | 11 | export type NavLink = { 12 | title: string 13 | href: string 14 | icon?: string 15 | } 16 | 17 | export type SidebarNavItem = { 18 | title: string 19 | disabled?: boolean 20 | external?: boolean 21 | icon?: keyof typeof Icons 22 | } & ( 23 | | { 24 | href: string 25 | items?: never 26 | } 27 | | { 28 | href?: string 29 | items: NavLink[] 30 | } 31 | ) 32 | 33 | export type DashboardConfig = { 34 | mainNav: MainNavItem[] 35 | sidebarNav: SidebarNavItem[] 36 | } -------------------------------------------------------------------------------- /types/siteConfig.ts: -------------------------------------------------------------------------------- 1 | export type AuthorsConfig = { 2 | name: string 3 | url: string 4 | } 5 | export type SiteConfig = { 6 | name: string 7 | description: string 8 | url: string 9 | keywords: string[] 10 | authors: AuthorsConfig[] 11 | creator: string 12 | ogImage: string 13 | links: { 14 | twitter: string 15 | github?: string 16 | }, 17 | metadataBase: URL 18 | themeColor: string 19 | icons: { 20 | icon: string 21 | shortcut: string 22 | apple: string 23 | } 24 | openGraph: { 25 | type: string 26 | locale: string 27 | url: string 28 | title: string 29 | description: string 30 | siteName: string 31 | }, 32 | twitter: { 33 | card: string 34 | title: string 35 | description: string 36 | images: string[] 37 | creator: string 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /types/subscribe.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@/types/user"; 2 | 3 | export interface Subscription { 4 | isPopular?: boolean; 5 | title: string; 6 | description: string; 7 | amount: number | string; 8 | expireType?: 'day' | 'week' | 'month' | 'year'; 9 | possess: string[]; 10 | mainClassName?: string; 11 | buttonClassName?: string; 12 | buttonText?: string; 13 | }; 14 | 15 | export interface SubscribeInfo { 16 | [key: string]: Subscription; 17 | }; 18 | 19 | export interface CreateCheckoutResponse { 20 | checkoutURL: string; 21 | }; 22 | 23 | export interface SubScriptionInfo { 24 | subscriptionId: string | number; 25 | membershipExpire: number; 26 | customerId: string; 27 | variantId: number; 28 | role: Role; 29 | isCanceled?: boolean; 30 | updatePaymentMethodURL?: string; 31 | customerPortal?: string; 32 | } 33 | export type UpgradeType = 'subscription' | 'single'; 34 | 35 | export type VariantIdsByType = { 36 | [key in UpgradeType]: string; 37 | }; 38 | 39 | // billing 页面显示的内容 40 | export interface UserSubscriptionPlan extends SubScriptionInfo { 41 | name: string 42 | description: string 43 | isPro: boolean 44 | } 45 | export interface LemonsqueezySubscriptionURLPatch { 46 | update_payment_method: string 47 | customer_portal: string 48 | } 49 | -------------------------------------------------------------------------------- /types/usage.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface DateRemaining { 3 | userTodayRemaining: number; // 今天剩余次数 4 | boostPackRemaining: number; // 加油包剩余次数 5 | userDateRemaining: number; // 上面二者总的剩余次数 6 | } 7 | -------------------------------------------------------------------------------- /types/user.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | 3 | export type Role = 0 | 2; // 0 Standard User; 2 Member User 4 | 5 | export type RedisUserId = string | null 6 | 7 | export interface UserId { 8 | userId: string; 9 | } 10 | 11 | export interface RemainingParams { 12 | userId: string; 13 | role?: Role; 14 | } 15 | 16 | export interface UserInfo { 17 | userId: string; 18 | username: string; 19 | avatar?: string; 20 | platform: string; 21 | email: string; 22 | role: Role; 23 | membershipExpire?: number; 24 | accessToken?: string; 25 | } 26 | 27 | export interface PrismaUser extends User { } --------------------------------------------------------------------------------4 | https://www.smartexcel.cc/sitemap-0.xml