Compare commits

...

497 Commits

Author SHA1 Message Date
javayhu
2a6e322c0a Merge remote-tracking branch 'origin/main' into cloudflare 2025-09-05 22:23:04 +08:00
javayhu
37f011cf74 refactor: rename getSocialLinks to useSocialLinks for improved clarity and consistency 2025-09-05 00:07:07 +08:00
javayhu
35d0ca9e12 refactor: rename getSidebarLinks to useSidebarLinks for improved clarity and consistency 2025-09-05 00:06:40 +08:00
javayhu
34baf20b31 refactor: rename getNavbarLinks to useNavbarLinks for improved clarity and consistency 2025-09-05 00:06:19 +08:00
javayhu
28fcbae6a2 refactor: rename getFooterLinks to useFooterLinks for improved clarity and consistency 2025-09-05 00:06:00 +08:00
javayhu
fc8cea13cd refactor: rename getAvatarLinks to useAvatarLinks for improved clarity and consistency 2025-09-05 00:05:41 +08:00
javayhu
6065c4af06 refactor: rename getPricePlans to usePricePlans for improved clarity and consistency 2025-09-05 00:05:10 +08:00
javayhu
ba7b950c01 refactor: rename getCreditPackages to useCreditPackages for improved clarity and consistency 2025-09-05 00:04:29 +08:00
javayhu
c94784e711 fix: add conditional rendering for payment data in credit packages 2025-09-05 00:00:16 +08:00
javayhu
48c045fb73 fix: add loading state handling in credit packages 2025-09-04 23:46:32 +08:00
javayhu
3fd47869a2 chore: update VSCode settings to exclude additional file types 2025-09-04 23:00:53 +08:00
javayhu
e3ac4a0a29 Merge remote-tracking branch 'origin/main' into cloudflare 2025-09-03 01:19:48 +08:00
javayhu
47adbcfd06 refactor: move premium related components to new folder 2025-09-03 01:10:03 +08:00
javayhu
5d5eb82013 feat: docs support premium content 2025-09-03 01:09:01 +08:00
javayhu
b0a065ced9 Merge remote-tracking branch 'origin/main' into cloudflare 2025-09-03 00:08:21 +08:00
javayhu
794c18a7e6 fix: fix localized callback url after login 2025-09-02 23:51:29 +08:00
javayhu
9899e1d164 fix: update billing card to reflect period end date and adjust related translations 2025-09-02 23:08:37 +08:00
javayhu
ad1cbedb56 Merge remote-tracking branch 'origin/main' into cloudflare 2025-09-02 00:18:06 +08:00
javayhu
3707500ed8 feat: optimize fetching subscription period start and end time 2025-09-02 00:16:10 +08:00
javayhu
f36018945d fix: change message component background color 2025-09-01 23:47:31 +08:00
javayhu
9f5d4aec59 chore: update ai elements 2025-09-01 23:44:18 +08:00
javayhu
e3f44a85a5 Merge remote-tracking branch 'origin/main' into cloudflare 2025-09-01 00:14:08 +08:00
javayhu
1f9a7c2621
Merge pull request #86 from MkSaaSHQ/dev/blog-premium
feat: premium content in blog posts
2025-08-31 22:03:43 +08:00
javayhu
a92ef86a71 fix: clarify test card number format and clean up imports in page component 2025-08-31 21:58:05 +08:00
javayhu
e2dfab2ca7 fix: update categories from "development" to "product" in premium blog post files 2025-08-31 21:44:14 +08:00
javayhu
e5061b3b67 custom: correct typo in source.config.ts and add premium content translations 2025-08-31 21:40:30 +08:00
javayhu
4faa89c0ee custom: replace premium Fumadocs blog post and integrate premium badge display 2025-08-31 21:40:09 +08:00
javayhu
481f3268db feat: implement premium access checks and enhance premium content handling in blog posts 2025-08-31 21:39:52 +08:00
javayhu
66d7dd3259 feat: add premium content feature with related components and configuration 2025-08-31 21:39:40 +08:00
javayhu
9aeb59dff2 chore: update .dockerignore, .gitignore, and biome.json to include .conductor directory 2025-08-31 16:50:13 +08:00
javayhu
2faedc2043 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-30 22:30:44 +08:00
javayhu
c0aa979382 chore: clean up imports and improve formatting in CreditsPageClient component 2025-08-30 00:24:12 +08:00
javayhu
fa2e981c16 fix: fix URL params when switch to balance from transactions 2025-08-28 23:23:52 +08:00
javayhu
0c415ee24b Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-28 10:09:34 +08:00
javayhu
21eee041ab
Merge pull request #85 from MkSaaSHQ/dev/nuqs
feat: integrate with nuqs for users and credit transaction tables
2025-08-28 01:10:48 +08:00
javayhu
6c584c75e2 feat: enhance CreditTransactions component with loading skeleton and refactor state management using useQueryStates 2025-08-28 01:08:48 +08:00
javayhu
797ee9b7e5 feat: add nuqs package and integrate NuqsAdapter in layout and users page components 2025-08-28 00:47:37 +08:00
javayhu
658409cfbd Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-27 00:53:02 +08:00
javayhu
422c323467 chore: replace Image component with BlogImage for improved loading handling in BlogCard 2025-08-27 00:45:09 +08:00
javayhu
de7e87e5b8 chore: update Skeleton component styles 2025-08-27 00:35:00 +08:00
javayhu
613bbd0d78 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-26 00:50:03 +08:00
javayhu
4434f1900d chore: remove deprecated Discord widget and related configurations 2025-08-26 00:48:39 +08:00
javayhu
895e02bfdd chore: update .dockerignore and biome.json to include new directories and remove unused object-values type 2025-08-26 00:20:27 +08:00
javayhu
7cc1fd5835 refactor: update website configuration structure to use 'ui' instead of 'metadata' for theme and mode settings 2025-08-26 00:16:55 +08:00
javayhu
4bad9714fa refactor: remove BlockCategory pages, and BlockPreview components 2025-08-25 23:43:45 +08:00
javayhu
fa4b9a19a1 refactor: remove service worker and related registration utilities 2025-08-25 23:39:10 +08:00
javayhu
1c0c46fa34 Revert "feat: enhance credit hooks to trigger updates on store changes"
This reverts commit 7851a715a3.
2025-08-25 10:01:54 +08:00
javayhu
0ae3f27c78 refactor: change default website mode from 'system' to 'dark' in website configuration 2025-08-25 10:00:45 +08:00
javayhu
fc024ea0da refactor: simplify layout of ChatBot and ImagePlayground components, update model configuration in web content analyzer 2025-08-25 10:00:28 +08:00
javayhu
80851fcf44 refactor: remove credit-related functionality and components from the web content analysis module 2025-08-25 09:48:20 +08:00
javayhu
31829ce17b feat: add ConsumeCreditsCard component for credit consumption functionality 2025-08-25 09:23:27 +08:00
javayhu
7c9b0a2697 refactor: remove ConsumeCreditCard component and integrate its functionality into ConsumeCreditsCard 2025-08-25 09:22:56 +08:00
javayhu
5f14259197 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-24 22:34:35 +08:00
javayhu
15da1ee48a refactor: update global styles with a custom theme inspired by Qoder, adjusting color variables and shadow effects 2025-08-24 21:16:41 +08:00
javayhu
c2d7e51f5b refactor: update HeroSection component styles to use accent color for hover state and simplify layout 2025-08-24 21:15:48 +08:00
javayhu
00405d5335 refactor: update text colors in BlogPostPage, FeaturesSection, and HeaderSection components to use primary color 2025-08-24 21:10:08 +08:00
javayhu
610346055f refactor: update navbar component styles to use accent foreground for active and hover states 2025-08-24 21:01:36 +08:00
javayhu
cb9c3132fd refactor: update CardFooter background color from bg-background to bg-muted across multiple components 2025-08-24 20:34:50 +08:00
javayhu
32fc3d6dc9 chore: simplify BlogCard component by removing unnecessary span styling and enhancing border effects 2025-08-24 20:11:24 +08:00
javayhu
69143ace47 feat: implement error handling in getCreditBalanceAction and add CreditsTest component for credit consumption testing 2025-08-24 19:55:53 +08:00
javayhu
8c3ef9bfaf refactor: remove unused heading elements from AIAudioPage and AIVideoPage components 2025-08-24 19:41:02 +08:00
javayhu
7851a715a3 feat: enhance credit hooks to trigger updates on store changes 2025-08-24 19:26:54 +08:00
javayhu
0fb4ef93d2 chore: add CreditsTest component 2025-08-24 19:26:49 +08:00
javayhu
95a6f3b9d5 feat: update AI Chat, add ChatBot component 2025-08-24 15:36:20 +08:00
javayhu
0794c7d297 feat: add AI Chat route and localization support in English and Chinese 2025-08-24 11:39:04 +08:00
javayhu
395f753025 refactor: remove TypeScript error comments from CodeBlock component 2025-08-24 11:32:43 +08:00
javayhu
fc53045d99
Merge pull request #82 from MkSaaSHQ/dev/ai-elements
feat: AI Chat demo with ai elements
2025-08-24 11:27:58 +08:00
javayhu
64ba2711aa feat: implement AI chat functionality with new API route and chat component 2025-08-24 11:26:06 +08:00
javayhu
3a61c953a4 chore: add ai-elements to biome.json for component inclusion 2025-08-24 10:39:52 +08:00
javayhu
6de7dfebf3 chore: update shadcnui badge and scroll-area components 2025-08-24 10:38:30 +08:00
javayhu
e626bb9af4 feat: add ai-elements components 2025-08-24 10:37:37 +08:00
javayhu
33fe00b8dc chore: mark lastRefreshAt field as deprecated in userCredit schema 2025-08-24 10:22:31 +08:00
javayhu
2d0392db61 refactor: update credit eligibility checks to use canAddCreditsByType function for improved clarity and maintainability 2025-08-24 10:03:35 +08:00
javayhu
afdaeba2be refactor: remove updateUserLastRefreshAt function and its calls to streamline credit update logic 2025-08-24 09:56:19 +08:00
javayhu
258ddad399 refactor: consolidate credit addition logic into a single function to improve maintainability and clarity 2025-08-24 09:24:59 +08:00
javayhu
e6bc1ea9e8 refactor: rename 'credits' to 'amount' in credit-related configurations and components for consistency 2025-08-24 01:16:39 +08:00
javayhu
96d630f3ac chore: add new hostname configuration for service.firecrawl.dev in Next.js config 2025-08-24 00:52:06 +08:00
javayhu
e15d76461f feat: add function to check if subscription credits can be added based on last refresh time 2025-08-23 20:21:54 +08:00
javayhu
1ff42009d8 chore: update function parameters planId instead of priceId 2025-08-23 20:07:32 +08:00
javayhu
669ac94bad chore: update comments to reflect renaming of enableForFreePlan to enablePackagesForFreePlan for clarity 2025-08-23 17:18:37 +08:00
javayhu
d319bd8af2 chore: rename enableForFreePlan to enablePackagesForFreePlan for clarity in credits configuration 2025-08-23 10:20:30 +08:00
javayhu
01f5734dd5 chore: update credit expiration messaging and logic to reflect upcoming expiration in days 2025-08-23 09:52:28 +08:00
javayhu
6837c5a8d4 chore: simplify CreditsBalanceCard layout 2025-08-23 09:05:12 +08:00
javayhu
6927f4b234 chore: adjust skeleton component heights in billing and password cards for improved loading state visibility 2025-08-23 09:00:13 +08:00
javayhu
ffe5bc4ea5 chore: update PasswordCardWrapper to include CardFooter with skeleton loading state 2025-08-23 08:55:02 +08:00
javayhu
1be26638fc chore: update billing card rendering logic to include payment data check 2025-08-23 08:23:54 +08:00
javayhu
c66fedea27 chore: update upgrade card visibility logic to ensure data is loaded before rendering 2025-08-23 07:51:37 +08:00
javayhu
b4dab95c04 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-23 00:14:47 +08:00
javayhu
1e1cafff32 chore: delete loading component for protected routes 2025-08-23 00:08:33 +08:00
javayhu
23ddb90e1f chore: remove loading components for protected routes 2025-08-22 23:52:58 +08:00
javayhu
8221f1753f Merge branch 'cloudflare' of https://github.com/MkSaaSHQ/mksaas-template into cloudflare 2025-08-22 01:17:47 +08:00
javayhu
18691030e7 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-22 01:17:38 +08:00
javayhu
7f4a7a61a2 refactor: enhance captcha handling in login and register forms with reset functionality 2025-08-22 01:12:13 +08:00
javayhu
a6a5d92dc1 refactor: implement batch processing for expired credits in credit cron job 2025-08-22 00:39:17 +08:00
javayhu
63a5e4f328 refactor: remove subscription and lifetime credits logic from credit stats and balance card 2025-08-22 00:04:40 +08:00
javayhu
19120ee7f1 refactor: replace password card logic with useHasCredentialProvider hook 2025-08-21 23:54:28 +08:00
javayhu
7aa7cb5603 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-21 10:04:48 +08:00
javayhu
d644611afd refactor: remove TANSTACK_QUERY_REFACTOR.md 2025-08-21 10:04:27 +08:00
javayhu
ca30f95027 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-21 09:59:28 +08:00
javayhu
8cc16a898c
Merge pull request #81 from MkSaaSHQ/dev/tanstack-query
feat: add tanstack query to optimize performance
2025-08-21 09:55:45 +08:00
javayhu
cf8a7f1242 refactor: optimize credits balance card logic after successful payment 2025-08-21 09:50:40 +08:00
javayhu
1fb89a2a05 refactor: enhance credit balance and stats hooks with improved error handling and logging 2025-08-21 01:48:40 +08:00
javayhu
13c23dab56 refactor: migrate state management from Zustand to TanStack Query for improved data fetching and caching across components 2025-08-21 00:54:40 +08:00
javayhu
ac8d4dee4b refactor: replace usePayment hook and store with useCurrentPlan for improved payment state management 2025-08-21 00:50:12 +08:00
javayhu
ff1e72df13 refactor: replace useCredits hook with custom hooks for credit balance, consumption, and transactions management 2025-08-21 00:27:26 +08:00
javayhu
d153ca655e refactor: implement custom hooks for user management and ban/unban user 2025-08-20 23:52:06 +08:00
javayhu
c00223c79a refactor: replace server actions with custom hooks for newsletter management and improve loading/error handling 2025-08-20 22:39:20 +08:00
javayhu
d59be1044a feat: add QueryProvider to manage React Query client and wrap Providers component 2025-08-20 22:37:52 +08:00
javayhu
5431160d62 chore: add @tanstack/react-query-devtools 2025-08-20 22:37:39 +08:00
javayhu
73baf946bd chore: add @tanstack/react-query and @tanstack/eslint-plugin-query 2025-08-20 22:04:19 +08:00
javayhu
d747683f82 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-20 00:20:22 +08:00
javayhu
978f13a368 refactor: remove unnecessary Skeleton component in billing and credits balance cards 2025-08-20 00:18:02 +08:00
javayhu
779493965c chore: remove images in about page 2025-08-20 00:13:26 +08:00
javayhu
3ae0411a44 chore: remove bg div in features section 2025-08-19 23:25:28 +08:00
javayhu
ccf064b0d5 chore: optimize pricing card badge 2025-08-19 23:09:41 +08:00
javayhu
b55613b471 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-18 00:59:21 +08:00
javayhu
7c0e6a5131 chore: enhance error handling and optimize database queries in user credit functions 2025-08-18 00:58:46 +08:00
javayhu
d86f89e3de fix: update URL routing in credits balance card component after successful payment 2025-08-18 00:45:51 +08:00
javayhu
9800b1d842 refactor: update CardFooter in credits balance card component 2025-08-18 00:42:53 +08:00
javayhu
9db52f352b refactor: adjust spacing in credits page layout and comment out button in credits balance card 2025-08-18 00:36:42 +08:00
javayhu
47679ab91e Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-18 00:32:31 +08:00
javayhu
90757475ac refactor: implement credits page with balance and transactions tabs 2025-08-18 00:29:56 +08:00
javayhu
904dceec44 refactor: update billing and credits pages & move credit balance and packages to credits page 2025-08-18 00:19:35 +08:00
javayhu
f468638f49 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-17 23:27:24 +08:00
javayhu
f1d02720d0 refactor: restructure credit distribution logic and enhance user credit handling 2025-08-17 23:26:10 +08:00
javayhu
35ddf5e08e Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-17 08:45:27 +08:00
javayhu
bade6b620e refactor: enhance error handling in credit balance retrieval 2025-08-16 23:08:16 +08:00
javayhu
d1928575b3 refactor: replace createSafeActionClient with userActionClient for improved session handling across multiple actions 2025-08-16 23:00:21 +08:00
javayhu
262228d6e9 feat: add session validation for admin access in getUsersAction 2025-08-16 22:03:01 +08:00
javayhu
57b92cfe85 feat: add script of listing users emails 2025-08-16 13:51:12 +08:00
javayhu
9f2fd58eb0 feat: optimize loading state in login and register forms 2025-08-16 10:48:58 +08:00
javayhu
1f7c38f9f5 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-16 08:19:56 +08:00
javayhu
9f71c9942a chore: optimize the colors of componnets in home page 2025-08-16 01:39:11 +08:00
javayhu
e99d6da45c chore: update blog card category bg 2025-08-16 00:39:43 +08:00
javayhu
63dd4e52fb Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-15 23:03:55 +08:00
javayhu
debbb5abf5 feat: support disable storage module 2025-08-15 22:51:48 +08:00
javayhu
c5dfaafe61 chore: optimize distributing credits when user sign up 2025-08-15 22:40:59 +08:00
javayhu
df3f3aa895 feat: support disable newsletter module 2025-08-15 22:39:29 +08:00
javayhu
a1ae6ca384 feat: support disable docs module 2025-08-15 22:23:28 +08:00
javayhu
866988d73c feat: support disable blog module 2025-08-15 22:20:15 +08:00
javayhu
46fd529390 refactor: add isDemo function 2025-08-15 22:10:01 +08:00
javayhu
fbb9a1b053 chore: update size of youtube video component 2025-08-15 20:25:37 +08:00
javayhu
200a9963f7 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-15 00:41:00 +08:00
javayhu
f6a2df402e fix: fix rate limit for newsletter subscription when sign up 2025-08-15 00:37:30 +08:00
javayhu
0da8f7d335 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-15 00:10:26 +08:00
javayhu
335c3b46d6 fix: fix build error in newsletter form validation 2025-08-15 00:06:28 +08:00
javayhu
004edeecea Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-14 23:14:32 +08:00
javayhu
f2b5bae866 feat: upgrade react-hook-form & zod & @hookform/resolvers
fix zod validation errors in form submission
2025-08-14 23:12:44 +08:00
javayhu
6bb12a2d86 Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-11 07:41:56 +08:00
javayhu
a1b54d7518 chore: update env example 2025-08-11 07:41:29 +08:00
javayhu
97654d97ea Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-11 07:36:28 +08:00
javayhu
064576f48e refactor: streamline user payment retrieval by utilizing a subquery for latest active payments 2025-08-10 15:54:46 +08:00
javayhu
0be53d3251 feat: optimize credit distribution by batching user processing and enhancing database queries 2025-08-10 14:26:53 +08:00
javayhu
564efbd3e2 feat: increase maxDuration for API functions in vercel.json from 60 to 300 seconds 2025-08-10 14:02:49 +08:00
javayhu
2814f87578 feat: update user last refresh time and refine user selection criteria 2025-08-10 13:53:48 +08:00
javayhu
78f76f35b9 feat: add basic auth to distribute credits cron jobs 2025-08-10 11:48:53 +08:00
javayhu
f50f60443a
Merge pull request #79 from MkSaaSHQ/dev/cron-jobs
support cron jobs
2025-08-10 11:16:48 +08:00
javayhu
346d154604 feat: support cron jobs 2025-08-10 11:13:21 +08:00
javayhu
7985769871 refactor: remove inngest 2025-08-10 11:07:10 +08:00
javayhu
aa2e025270 cf: update cloudflare env types 2025-08-09 13:12:42 +08:00
javayhu
11bfcb731d cf: upgrade version of opennextjs and wrangler 2025-08-09 10:13:32 +08:00
javayhu
62eb4124be Merge remote-tracking branch 'origin/main' into cloudflare 2025-08-03 15:20:14 +08:00
javayhu
2b72570784 feat: support openrouter in ai text (not stable for now) 2025-08-03 11:17:39 +08:00
javayhu
3c3dcd5d2a feat: upgrade zod v4 & use error instead of message
https://zod.dev/v4/changelog?id=deprecates-message
2025-08-03 00:35:22 +08:00
javayhu
89fd7193ac feat: upgrade to zod v4 & fix z.url() and z.email()
https://zod.dev/v4/changelog?id=zstring-updates
2025-08-03 00:32:44 +08:00
javayhu
8b2f1848a8 feat: upgrade zod to v4 2025-08-02 01:00:15 +08:00
javayhu
d0ddc2b1b0 feat: upgrade ai sdk to v5
https://v5.ai-sdk.dev/docs/migration-guides/migration-guide-5-0
2025-08-02 00:54:34 +08:00
javayhu
6d4d316564 chore: remove google vertex ai 2025-08-02 00:22:02 +08:00
javayhu
7a61aa3dff chore: update google api key var name 2025-08-01 21:39:20 +08:00
javayhu
46ec614fd3 refactor: move CreditsProvider 2025-07-28 22:53:31 +08:00
javayhu
abb15de848 fix: reset enable update avatar by default 2025-07-26 22:52:34 +08:00
javayhu
ba2a2b5fb0 feat: support disable update avatar in settings 2025-07-26 22:50:53 +08:00
javayhu
2b8e0b9cb5 feat: add checks for Stripe environment variables in getActiveSubscriptionAction 2025-07-25 23:18:42 +08:00
javayhu
d7cc9b956d Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-24 01:17:23 +08:00
javayhu
0b695cc4b2 feat: add credits configuration checks in hooks and provider 2025-07-24 01:16:55 +08:00
javayhu
3cb0911cf4 fix: ensure current user is only displayed when component is mounted 2025-07-24 01:14:53 +08:00
javayhu
0d04f6914e refactor: optimize credits rendering by memoizing and moving checks before hooks 2025-07-24 00:51:57 +08:00
javayhu
22d68c005a refactor: optimize credits rendering by memoizing and moving checks before hooks 2025-07-24 00:50:23 +08:00
javayhu
70446d10b3 Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-24 00:10:11 +08:00
javayhu
bc4578a3cd chore: remove credits disabled check in credits components 2025-07-24 00:04:42 +08:00
javayhu
971b0d65a0 chore: support google gemini and deepseek in ai text demo 2025-07-23 00:26:58 +08:00
javayhu
313c783dbd Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-20 14:44:58 +08:00
javayhu
4384a1d43f feat: add session id to payment table & fix duplicate one time payment process 2025-07-20 14:38:20 +08:00
javayhu
7b9b7a0dd7 chore: handle internal docs link redirection for internationalization 2025-07-20 13:02:41 +08:00
javayhu
07ad39871f custom: support intl in a/card components of docs page 2025-07-20 12:56:06 +08:00
javayhu
3d4245e8bc feat: add indexes to database tables 2025-07-19 16:40:36 +08:00
javayhu
cc56f9d729 Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-19 15:15:42 +08:00
javayhu
5912849fa7
Merge pull request #71 from MkSaaSHQ/dev/ai-text
feat: add ai text demo
2025-07-19 14:51:03 +08:00
javayhu
3075681dc8 feat: add ai text demo & scraping and analyzing the web content from URL 2025-07-19 14:41:12 +08:00
javayhu
757f1dc4ae feat: support crisp chat 2025-07-18 23:33:07 +08:00
javayhu
1be38e3e8d feat: support disable credential login 2025-07-18 22:04:39 +08:00
javayhu
716eac324f chore: add captcha validation to login form 2025-07-18 21:15:55 +08:00
javayhu
bd029eac2a chore: disable turnstile and credits by default, enable on demo website 2025-07-18 20:41:43 +08:00
javayhu
2c4db1e744 chore: update Captcha component 2025-07-18 20:37:59 +08:00
javayhu
cb7743fe07 refactor: remove scrollbar hiding styles and adjust overflow for scroll-locked state 2025-07-14 22:37:25 +08:00
javayhu
3a81a96316 refactor: add opennext and wrangler folder to the ignore patterns in biome.json 2025-07-14 22:27:22 +08:00
javayhu
d7077cb3d4 refactor: enable useEditorconfig option to biome.json
https://biomejs.dev/reference/configuration/#formatteruseeditorconfig
2025-07-14 22:19:31 +08:00
javayhu
e5569dabd1 Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-13 23:04:14 +08:00
javayhu
0d5185a789 refactor: remove unused env NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY 2025-07-13 23:03:58 +08:00
javayhu
813d8ea0bb Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-13 23:02:55 +08:00
javayhu
a8c76d3249 refactor: integrate useMounted hook to fix hydration error in BillingCard and CreditsBalanceCard 2025-07-13 22:59:26 +08:00
javayhu
49b39ad9dd refactor: update credit checkout session callback param name 2025-07-13 22:40:20 +08:00
javayhu
1adf3d5dc3 refactor: simplify success toast handling and refresh logic in CreditsBalanceCard 2025-07-13 22:39:28 +08:00
javayhu
5e877bf45e refactor: update cache duration to 2 minutes 2025-07-13 21:43:15 +08:00
javayhu
4277970074 refactor: enhance logging in StripeProvider to improve security and clarity by hiding sensitive information 2025-07-13 15:44:03 +08:00
javayhu
b27d8cc505 refactor: add retry functionality for payment and credits data fetching with improved user experience 2025-07-13 15:15:44 +08:00
javayhu
2d2a85cd26 refactor: enhance fetch logic in useCredits and usePayment hooks for improved clarity and consistency 2025-07-13 14:31:35 +08:00
javayhu
c960738133 refactor: streamline session handling in payment and credits providers 2025-07-13 14:25:56 +08:00
javayhu
bf8993ca96 refactor: update payment renewal logic to enhance credit handling 2025-07-13 09:40:46 +08:00
javayhu
a89910489a refactor: rename addSubscriptionRenewalCredits to addSubscriptionCredits for clarity 2025-07-13 09:26:16 +08:00
javayhu
368644b434
Merge pull request #64 from MkSaaSHQ/dev/credits-v2
[feat] support credits v2
2025-07-13 00:57:08 +08:00
javayhu
f5e639bbc7 refactor: improve plan filtering in credits logic 2025-07-13 00:53:31 +08:00
javayhu
52aeb2d61c refactor: rename functions to remove 'IfNeed' suffix for clarity and consistency 2025-07-13 00:37:01 +08:00
javayhu
7af313868c refactor: rename resetState to resetCreditsState and remove unused updateBalanceOptimistically method 2025-07-13 00:36:02 +08:00
javayhu
4313e32471 refactor: conditionally render credits-related components based on configuration 2025-07-13 00:09:05 +08:00
javayhu
31116cbf8b refactor: remove unused Stripe dependency and update credit expiration logic 2025-07-12 23:41:50 +08:00
javayhu
8a08dfdf3b refactor: update feature labels and standardize FAQ component naming in zh.json 2025-07-12 22:42:03 +08:00
javayhu
141b562307 refactor: improve loading state management in billing page 2025-07-12 22:36:17 +08:00
javayhu
9fcfb3bdf7 refactor: update credit and transaction messages 2025-07-12 22:14:04 +08:00
javayhu
8be9f6c775 style: update text colors in billing and credits balance cards for improved visibility 2025-07-12 21:32:29 +08:00
javayhu
ac02ea780a feat: add credit statistics in credits balance card 2025-07-12 21:29:28 +08:00
javayhu
e3aa8eab55 feat: add support for free plan users in credit packages and update billing card layout 2025-07-12 20:23:23 +08:00
javayhu
72e0a14fc9 refactor: improve layout consistency in settings pages by adjusting grid structures 2025-07-12 19:46:27 +08:00
javayhu
0f79ed14f0 refactor: standardize string quotes and improve formatting in components 2025-07-12 19:29:47 +08:00
javayhu
ee341522f5 feat: enhance billing and credits management with new components and improved layout 2025-07-12 19:29:21 +08:00
javayhu
b4e8585929 chore: add credits when subscription renewal and lifetime payment 2025-07-12 17:27:35 +08:00
javayhu
b5997ded4c feat: add payment success message and improve billing card layout 2025-07-12 17:03:16 +08:00
javayhu
367965e41f feat: ensure handler session id only once in credit package 2025-07-12 12:46:57 +08:00
javayhu
c7a1ec69bb feat: add ConsumeCreditCard component for credit consumption 2025-07-12 11:08:28 +08:00
javayhu
4160305a67 chore: update env.example with Inngest keys 2025-07-12 09:59:30 +08:00
javayhu
a5c6c8b493 feat: display user email and customer ID in UserDetailViewer 2025-07-12 08:10:31 +08:00
javayhu
4abca022aa feat: restrict access to current user's transactions 2025-07-12 00:59:06 +08:00
javayhu
765f5e1e39 feat: add register gift and monthly refresh credits to new user 2025-07-12 00:55:27 +08:00
javayhu
9f3c5e80c2 feat: add daily credit distribution function and integrate with existing credits logic 2025-07-12 00:37:49 +08:00
javayhu
997c362ac9 chore: add inngest package and update client initialization 2025-07-11 23:53:46 +08:00
javayhu
bda2571a78 chore: invoke function from code in inngest 2025-07-11 23:31:54 +08:00
javayhu
788fbe2f18 chore: inngest explore 2025-07-11 23:31:45 +08:00
javayhu
f45bcad110 feat: update UsersTable component to enhance column sizing and loading state handling 2025-07-11 22:24:02 +08:00
javayhu
75db5e85a7 feat: enhance sorting functionality in tables by implementing dropdown menus 2025-07-11 22:10:04 +08:00
javayhu
9711d13804 refactor: optimize user fetching logic in UsersPageClient component by using useCallback for fetchUsers function 2025-07-11 01:38:54 +08:00
javayhu
9d4fcbe36d refactor: remove useCreditTransactionStore and related logic from credits components, streamline useCredits integration 2025-07-11 01:35:31 +08:00
javayhu
e6663b013d refactor: replace getCreditBalanceAction with useCredits hook in credits-related components 2025-07-11 01:10:43 +08:00
javayhu
5cb8b0048d feat: add CreditsProvider and credits store for managing user credits 2025-07-11 00:19:20 +08:00
javayhu
0b6f81aca6 refactor: replace useTransactionStore with useCreditTransactionStore in credit-related components 2025-07-10 23:27:43 +08:00
javayhu
6cf9d4db9c refactor: improve credit transaction filtering in processExpiredCredits and consumeCredits functions 2025-07-10 22:53:04 +08:00
javayhu
59c7c807db refactor: rename PURCHASE to PURCHASE_PACKAGE in transaction types 2025-07-10 22:52:26 +08:00
javayhu
de1ccca27b feat: implement consume credits action and get credit balance action, update credits balance references 2025-07-10 22:00:33 +08:00
javayhu
263440742a feat: add CreditDetailViewer component and enhance credit transaction details in English and Chinese 2025-07-10 21:43:43 +08:00
javayhu
b75e9eb282 refactor: initialize sorting state with default value in UsersPage and CreditTransactionsPage components 2025-07-10 19:40:08 +08:00
javayhu
2aeb027e2f feat: add subscription renewal and lifetime monthly messages in English and Chinese 2025-07-10 19:34:06 +08:00
javayhu
0500617803 style: adjust padding in dashboard header actions 2025-07-10 19:21:09 +08:00
javayhu
3872a9d422 refactor: remove session_id from URL parameters in CreditPackages component 2025-07-10 19:20:11 +08:00
javayhu
2e0a195a2a refactor: update URL handling in CreditPackages component 2025-07-10 16:48:29 +08:00
javayhu
95bd256bc7 chore: remove onPaymentIntentSucceeded in stripe 2025-07-10 16:31:57 +08:00
javayhu
6c1a4685cd refactor: add disabled to credits config 2025-07-10 16:30:56 +08:00
javayhu
bbae584c88 fix: update DashboardHeaderand update credits label in CreditsBalanceMenu to use translations 2025-07-10 15:56:15 +08:00
javayhu
f649db26ae feat: add CreditsBalanceButton and CreditsBalanceMenu components 2025-07-10 15:41:11 +08:00
javayhu
1c7848f6b0 refactor: change getAllCreditPackagesInServer to getAllCreditPackages 2025-07-10 15:06:58 +08:00
javayhu
5c213d014a refactor: 'popular' instead of 'recommended' for price plan 2025-07-10 14:54:07 +08:00
javayhu
861502c28f feat: implement credit distribution for all users based on subscription status 2025-07-10 14:52:23 +08:00
javayhu
737bd7f80f feat: enhance credits management with subscription renewal and lifetime monthly credits 2025-07-10 14:42:36 +08:00
javayhu
74d7cf44a1 Merge branch 'dev/credits-v2' of https://github.com/MkSaaSHQ/mksaas-template into dev/credits-v2 2025-07-10 10:54:10 +08:00
javayhu
e011d09803 feat: enhance CreditPackages component with loading indicator 2025-07-10 10:54:07 +08:00
javayhu
50c500deb5 refactor: streamline credit handling in StripeProvider
- Updated StripeProvider to retrieve priceId from session metadata instead of line items.
- Introduced credit package retrieval to include expiration information when adding credits.
- Enhanced logging to reflect credit expiration details during credit addition.
- Removed obsolete code related to priceId retrieval for improved clarity and maintainability.
2025-07-10 10:15:42 +08:00
javayhu
cd710bb9ed feat: support register gift credits 2025-07-10 09:58:48 +08:00
javayhu
04f7f891a4 feat: update credit expiration handling and configuration
- Added expireDays property to credit packages and related configurations in website.tsx for better management of credit expiration.
- Modified addCredits function to handle expireDays more flexibly, allowing for undefined values.
- Updated functions for adding register gift and monthly free credits to utilize the new expireDays configuration.
- Enhanced type definitions for credits to include optional expireDays for improved clarity.
- Removed obsolete creditExpireDays from the credits configuration to streamline the codebase.
2025-07-10 01:12:11 +08:00
javayhu
c67b804f4f Merge branch 'cloudflare' of https://github.com/MkSaaSHQ/mksaas-template into cloudflare 2025-07-10 00:48:21 +08:00
javayhu
a44e4a669c Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-10 00:48:17 +08:00
javayhu
5af1182a58 chore: enhance .gitattributes for consistent line endings across various file types 2025-07-09 23:07:00 +08:00
javayhu
24c0334911 chore: add .gitattributes file and update VSCode extensions 2025-07-09 22:51:50 +08:00
javayhu
3e0861f883 fix: update sidebar rendering logic and enhance credit package success toast
- Updated DashboardSidebar to conditionally render SidebarMain based on loading state.
- Modified CreditPackages to trigger a refresh of credits data and show success toast with a delay to avoid React lifecycle conflicts.
2025-07-09 21:58:35 +08:00
javayhu
da4b018e8d Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-09 19:14:30 +08:00
javayhu
a7738f0cbf feat: implement credit checkout session and enhance credit package management
- Added new credit checkout session functionality to facilitate credit purchases.
- Introduced credit package configurations in env.example for better management.
- Updated English and Chinese JSON files with new messages for checkout processes.
- Refactored existing components to utilize the new credit checkout session and streamline the credit purchasing workflow.
- Removed obsolete payment intent handling to simplify the codebase.
2025-07-09 00:22:07 +08:00
javayhu
adb9b80572 fix: update default expireDays assignment in addCredits function 2025-07-08 00:51:04 +08:00
javayhu
2e8f70dc76 feat: enhance credits management with new configurations
- Added credit expiration days, register gift credits, and free monthly credits options in website configuration.
- Updated credits handling functions to utilize the new configuration settings for improved flexibility and maintainability.
- Removed obsolete constants related to credits from constants.ts to streamline the codebase.
- Enhanced type definitions for credits configuration in index.d.ts for better clarity.
2025-07-08 00:48:17 +08:00
javayhu
b94fd34be5 Merge remote-tracking branch 'origin/main' into dev/credits 2025-07-08 00:32:08 +08:00
javayhu
0d4e8fe899 docs: add CLAUDE.md for project guidance 2025-07-08 00:30:40 +08:00
javayhu
73ce18f564 refactor: update credit package descriptions and improve component structure
- Revised credit package descriptions in the English JSON file for clarity and consistency.
- Refactored CreditPackages component to utilize a more efficient method for retrieving credit packages.
- Removed obsolete functions and streamlined the code for better maintainability and performance.
2025-07-07 01:10:06 +08:00
javayhu
e430a0c319 feat: implement credit packages management with translations and server integration
- Added new credit packages structure in English and Chinese JSON files for better localization.
- Introduced server-side functions to retrieve credit packages and package details.
- Updated client-side components to utilize new credit package retrieval methods.
- Refactored existing code to enhance modularity and maintainability by separating client and server logic.
- Removed obsolete credit package retrieval functions to streamline the codebase.
2025-07-07 00:47:43 +08:00
javayhu
f7f7be2ef0 feat: add credits config in website config
- Added a new credits management system with configurable credit packages in website.tsx.
- Replaced hardcoded credit package definitions with a dynamic retrieval system using getCreditPackages and getCreditPackageById functions.
- Updated CreditPackages and StripePaymentForm components to utilize the new credit package structure.
- Removed obsolete CREDIT_PACKAGES constant from constants.ts to streamline the codebase.
- Enhanced type definitions for credit packages in types.ts for better clarity and maintainability.
- Updated README.md to reflect changes in credit packages configuration.
2025-07-07 00:04:45 +08:00
javayhu
eafb3775e8 Merge remote-tracking branch 'origin/main' into dev/credits 2025-07-06 23:28:31 +08:00
javayhu
0af0aa3b09 refactor: reorganize credit transaction types and update imports
- Moved CREDIT_TRANSACTION_TYPE from constants to a new types.ts file for better modularity.
- Updated import paths in credit-related components to reflect the new structure.
- Removed the old CREDIT_TRANSACTION_TYPE definition from constants.ts to streamline the codebase.
2025-07-06 23:25:38 +08:00
javayhu
04c2b2d7ee refactor: update translations and improve table component structure
- Removed unused translation keys from English and Chinese JSON files.
- Updated UsersTable and CreditTransactionsTable components to utilize a centralized translation function for table-related messages.
- Initialized sorting state in both table components for consistent default behavior.
2025-07-06 18:20:31 +08:00
javayhu
d9cda3e122 feat: enhance credits settings with tabbed interface and improved translations
- Implemented a tabbed interface in the CreditsPage component to separate balance and transactions views.
- Updated CreditPackages and CreditTransactionsPageClient components to utilize the new tab structure.
- Enhanced translation support for credits-related messages in both English and Chinese.
- Improved error handling and user feedback in credit-related components.
- Refactored CreditTransactionsTable to utilize translations for table headers and pagination controls.
2025-07-06 17:17:24 +08:00
javayhu
d8a12343c8 refactor: adjust spacing in layout components for consistency
- Updated spacing from `space-y-10` to `space-y-8` in layout components for Billing, Credits, Notifications, Profile, and Security to ensure uniformity across the settings pages.
2025-07-06 11:30:10 +08:00
javayhu
40af0f6922 feat: add credit transactions table
- Introduced a new CreditTransactionsPageClient component to display credit transactions.
- Implemented getCreditTransactions action for fetching transaction data.
- Added CreditTransactionsTable component for rendering transaction details with pagination and sorting.
- Updated English and Chinese translation files to include credit transaction messages.
- Integrated the credit transactions page into the existing credits settings layout.
2025-07-06 10:44:13 +08:00
javayhu
1740c826c7 feat: add CreditsBalance component and integrate into dashboard and navbar
- Introduced a new CreditsBalance component to display user credits.
- Integrated CreditsBalance into DashboardHeader, Navbar, and NavbarMobile for improved visibility of user credits.
- Enhanced user interaction by allowing navigation to the credits settings page.
2025-07-06 00:41:57 +08:00
javayhu
75083b32e4 refactor: reorganize credit-related imports and enhance user feedback
- Updated import paths for credit-related actions and functions to improve module organization.
- Removed redundant refresh trigger declaration in CreditPackages component.
- Simplified success toast message in CreditPackages and PaymentForm components for clarity.
- Introduced a new credits.ts file to centralize credit management logic and improve maintainability.
2025-07-06 00:15:07 +08:00
javayhu
e933844479 feat: enhance credit management with transaction store and UI updates
- Added a new transaction store to manage refresh triggers for credit-related components.
- Updated CreditPackages and StripePaymentForm components to utilize the transaction store for refreshing UI after credit purchases.
- Modified .gitignore to include certificates.
- Introduced a new script in package.json for running the development server with HTTPS support.
2025-07-05 23:59:30 +08:00
javayhu
13bee49f90 feat: add credits section to avatar configuration and update translations
- Added "Credits" entry to the avatar configuration for navigation.
- Updated English and Chinese translation files to include "Credits" label.
- Refactored error messages in credit payment actions for clarity.
- Enhanced loading state management in CreditPackages component.
- Replaced icons in CreditPackages component for improved UI consistency.
2025-07-05 23:25:37 +08:00
javayhu
fe2b1bbe39 feat: add credit purchase functionality with Stripe integration
- Introduced credit purchase payment intent actions in credits.action.ts.
- Created new components for credit packages and Stripe payment form.
- Added routes and layout for credits settings page.
- Updated sidebar configuration to include credits settings.
- Enhanced constants for credit packages with detailed pricing and descriptions.
- Implemented loading and layout components for credits page.
- Integrated payment confirmation handling in Stripe provider.
2025-07-05 22:30:22 +08:00
javayhu
8a9c76c628 fix: add missing newline at end of index.zh.mdx file 2025-07-05 17:11:07 +08:00
javayhu
98421afab8 Merge remote-tracking branch 'origin/main' into dev/credits 2025-07-05 15:52:40 +08:00
javayhu
6980507c43 chore: update dashboard and setting pages layout 2025-07-05 15:51:36 +08:00
javayhu
8657bf4e84 chore: show theme selector in demo website only 2025-07-05 10:10:39 +08:00
javayhu
bab58e6420 chore: add @stripe/react-stripe-js 2025-07-05 00:25:11 +08:00
javayhu
8d17bd80c5 feat: implement admin access control in UsersLayout 2025-07-04 23:23:25 +08:00
javayhu
c7e3de816c chore: update credit related functions 2025-07-04 01:02:56 +08:00
javayhu
da26c2cf5d Merge branch 'main' of https://github.com/MkSaaSHQ/mksaas-template into dev/credits 2025-07-02 23:46:20 +08:00
javayhu
b838ddc293 Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-02 22:53:01 +08:00
javayhu
cc9a15db8f chore: make DiscordWidget deprecated 2025-07-02 22:52:32 +08:00
javayhu
8e63af3e7f Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-02 01:15:29 +08:00
javayhu
aaadd7fcf4 fix: add type assertion for image generation response 2025-07-02 01:14:58 +08:00
javayhu
1e2e4d77f7 Merge remote-tracking branch 'origin/main' into cloudflare 2025-07-02 00:54:31 +08:00
javayhu
88bb977c7b Merge branch 'dev/credits' of https://github.com/MkSaaSHQ/mksaas-template into dev/credits 2025-07-01 00:23:25 +08:00
javayhu
abf8b31ec7 chore: optimize the credit functions 2025-07-01 00:23:21 +08:00
javayhu
12fb19e97b chore: add credit related tables 2025-07-01 00:23:21 +08:00
javayhu
d8904750d9 chore: fix db instance 2025-07-01 00:23:21 +08:00
javayhu
b30355dfe5 chore: update credit related functions (2) 2025-07-01 00:23:21 +08:00
javayhu
dae7a3b0e8 chore: update credits related functions (2) 2025-07-01 00:23:21 +08:00
javayhu
e0c0ff9518 chore: update credits related functions 2025-07-01 00:23:21 +08:00
javayhu
e1b0e2f44c feat: support credits 2025-07-01 00:23:21 +08:00
javayhu
6195df2bc5 chore: update readme 2025-07-01 00:02:51 +08:00
javayhu
3fa44f92c7 chore: add config for enableTurnstileCaptcha 2025-06-30 23:50:56 +08:00
javayhu
55ae5ced9e fix: fix ai image default models 2025-06-30 23:50:01 +08:00
javayhu
111568d746 fix: show captcha when turnstile is enabled only 2025-06-30 23:05:38 +08:00
javayhu
45e6a59fe6 chore: optimize the credit functions 2025-06-30 01:10:14 +08:00
javayhu
684bbdff82 chore: add credit related tables 2025-06-30 00:25:26 +08:00
javayhu
181e478bc3 chore: fix db instance 2025-06-30 00:05:25 +08:00
javayhu
82d0fa1061 Merge branch 'main' of https://github.com/MkSaaSHQ/mksaas-template into dev/credits 2025-06-28 23:12:38 +08:00
javayhu
ba3cbe0724
Merge pull request #58 from MkSaaSHQ/dev/ai-image
feat: ai image generator
2025-06-28 22:57:30 +08:00
javayhu
91614ed6e5 fix: fix build error & remove Spinner 2025-06-28 22:50:40 +08:00
javayhu
0453db5ec6 chore: optimize ai image generator 2025-06-28 22:47:40 +08:00
javayhu
1a297e33f9 Merge branch 'main' of https://github.com/MkSaaSHQ/mksaas-template into dev/ai-image 2025-06-28 10:12:23 +08:00
javayhu
05b90fb0a7 chore: optimize validate captcha 2025-06-28 10:09:50 +08:00
javayhu
66567cfecd chore: optimize turnstile captcha 2025-06-28 00:34:31 +08:00
javayhu
46d008e5fc chore: update readme 2025-06-28 00:01:53 +08:00
javayhu
c7cbf96a70
Merge pull request #57 from MkSaaSHQ/dev/turnstile
feat: support cloudflare turnstile
2025-06-27 23:52:18 +08:00
javayhu
958852335d feat: support cloudflare turnstile 2025-06-27 23:50:56 +08:00
javayhu
3058484803 custom: optimize ai image generator page 2025-06-27 01:05:19 +08:00
javayhu
985579b964 feat: support ai image generator 2025-06-26 00:41:27 +08:00
javayhu
b3180e617d chore: support datafast analytics revenue track 2025-06-25 20:28:09 +08:00
javayhu
bc915a53dc chore: add monitor api route 2025-06-24 23:48:43 +08:00
javayhu
bd67ac3517 chore: support allow promotion code in price config 2025-06-24 23:00:42 +08:00
javayhu
e94625ce4e custom: add command cf-dev on port 8787 2025-06-22 11:58:13 +08:00
javayhu
e70a8c92a2 Merge remote-tracking branch 'origin/main' into dev/credits 2025-06-21 22:50:09 +08:00
javayhu
2153cf6771 chore: add dev.vars example file 2025-06-21 17:35:32 +08:00
javayhu
0164c833db Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-21 14:40:23 +08:00
javayhu
cfbfa409b0 Merge branch 'main' of https://github.com/MkSaaSHQ/mksaas-template 2025-06-21 10:47:37 +08:00
javayhu
ffbe7f4fb0 chore: optimize docker deployment by add docker ignore file 2025-06-21 10:47:29 +08:00
javayhu
13a79c7a01
Merge pull request #51 from MkSaaSHQ/dev/docker
feat: support docker & dokploy deployment
2025-06-21 09:41:15 +08:00
javayhu
1cb0793a83 chore: change baseURL in fetch session 2025-06-21 09:27:22 +08:00
javayhu
5e1f9167e0 fix: fix build error Cannot find config file 2025-06-21 01:23:31 +08:00
javayhu
39a2870131 chore: support docker deployment 2025-06-21 01:02:27 +08:00
javayhu
40b313a2f2 chore: move mdx-components to docs folder 2025-06-20 23:51:25 +08:00
javayhu
9e54932b27 chore: move source.ts to lib folder 2025-06-20 23:12:29 +08:00
javayhu
5d50135ed6 Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-20 22:21:04 +08:00
javayhu
befd10cc5b refactor: remove useless packages 2025-06-20 21:17:11 +08:00
javayhu
b17599976f chore: move blog types to index 2025-06-20 21:09:50 +08:00
javayhu
107f761716 chore: optimize email templates (found by knip) 2025-06-20 21:00:51 +08:00
javayhu
cb5c588b1e chore: install knip 2025-06-20 20:58:24 +08:00
javayhu
cbfe5e433d Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-20 02:04:54 +08:00
javayhu
7c101d595e refactor(storage) replace with s3mini sdk & fix upload issue in cloudflare worker 2025-06-20 02:04:14 +08:00
javayhu
196f72ff68 chore: fix lint and format issues 2025-06-20 02:04:00 +08:00
javayhu
7ab7d2d504 refactor(storage) replace with s3mini sdk & fix upload issue in cloudflare worker 2025-06-20 02:03:22 +08:00
javayhu
522d8de4ee chore: add comments for nodejs_compat 2025-06-20 01:36:35 +08:00
javayhu
0739c717d8 chore: fix lint and format issues 2025-06-20 01:18:48 +08:00
javayhu
71b9807433 Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-19 00:40:41 +08:00
javayhu
e49d4624fe fix: fix docs sidebar collapse issue 2025-06-18 23:56:28 +08:00
javayhu
8a72fb2409 chore: revert change website name 2025-06-18 00:18:58 +08:00
javayhu
e00c22d0fe Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-18 00:17:09 +08:00
javayhu
8a5a49f041 fix: add type annotation for data in GitHubStarsButton component 2025-06-18 00:15:20 +08:00
javayhu
bd8ccf4cf3 Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-18 00:09:28 +08:00
javayhu
fd4426ddd7 chore: remove useless packages 2025-06-17 23:41:16 +08:00
javayhu
fcb58d2206 refactor(blog) remove blog toc component 2025-06-17 23:38:56 +08:00
javayhu
aa547f209e chore: adjust locale handling in Providers and DocsRootLayout, and simplify search API request handling 2025-06-17 23:38:45 +08:00
javayhu
5b30fd8c48 chore: update DynamicCodeBlock component props 2025-06-17 23:38:38 +08:00
javayhu
c8b29c59dc chore: fix fumadocs top empty banner shown when in cloudflare worker env 2025-06-17 23:38:34 +08:00
javayhu
d946e2aead chore: upgrade fumadocs ui and core 2025-06-17 23:38:30 +08:00
javayhu
ddd3ee07df cf: do not remove logs in prod env 2025-06-17 23:38:24 +08:00
javayhu
da0176ffc5 refactor(blog) update sitemap for blog pages 2025-06-17 23:38:11 +08:00
javayhu
568ef9bc3a refactor(blog) blog category pages 2025-06-17 23:38:07 +08:00
javayhu
e05d20ee5e refactor(blog) optimize inline toc & blog page layout 2025-06-17 23:38:02 +08:00
javayhu
56fa6fb63c refactor(blog) blog page ready with toc 2025-06-17 23:37:56 +08:00
javayhu
ebeacae587 refactor(blog) refactor blog home page 2025-06-17 23:37:52 +08:00
javayhu
7d5f4a52a8 refactor(blog) update date in mdx files 2025-06-17 23:37:47 +08:00
javayhu
0a2d081b07 refactor(blog) remove content-collections & add blog source 2025-06-17 23:37:41 +08:00
javayhu
21bc0b1293 refactor(blog) update content about blog posts 2025-06-17 23:37:02 +08:00
javayhu
c477aae333 refactor(pages) migrate custom pages to using fumadocs 2025-06-17 23:36:57 +08:00
javayhu
543798e2c1 refactor(changelog) refactor release card component 2025-06-17 23:36:50 +08:00
javayhu
483a970b71 refactor(changelog) migrate changelog to use fumadocs 2025-06-17 23:36:44 +08:00
javayhu
292faddc7a refactor(docs) parse and render docs mdx files by fumadocs 2025-06-17 23:36:09 +08:00
javayhu
8fd3b679fb refactor(docs) remove math and package-install as code block languages 2025-06-17 23:35:43 +08:00
javayhu
2d12d89e3b chore: add fumadocs-mdx 2025-06-17 23:35:36 +08:00
javayhu
d0aef4b7d4 chore: remove useless packages 2025-06-17 23:26:33 +08:00
javayhu
c006ee750d refactor(blog) remove blog toc component 2025-06-17 23:26:02 +08:00
javayhu
19a6c4d994 cf: fix stripe api issue by setting httpClient 2025-06-17 22:35:27 +08:00
javayhu
86f13a1748 chore: reset compatibility_flags to nodejs_compat & nodejs_compat_v2 not solve file upload issue 2025-06-17 22:15:12 +08:00
javayhu
745ba457df cf: add init open next cf for dev 2025-06-17 22:14:01 +08:00
javayhu
beb53639a3 chore: adjust locale handling in Providers and DocsRootLayout, and simplify search API request handling 2025-06-17 21:44:48 +08:00
javayhu
65fb8722bc chore: update DynamicCodeBlock component props 2025-06-17 21:28:36 +08:00
javayhu
160a7eb929 chore: fix fumadocs top empty banner shown when in cloudflare worker env 2025-06-17 21:17:34 +08:00
javayhu
c3d82d9183 chore: upgrade fumadocs ui and core 2025-06-17 21:16:46 +08:00
javayhu
767351c5cd cf: add nodejs_compat_v2 to compatibility_flags 2025-06-17 20:43:54 +08:00
javayhu
fd3c82baaf cf: do not remove logs in prod env 2025-06-17 20:06:20 +08:00
javayhu
168eae946f cf: enable worker log push 2025-06-17 19:41:18 +08:00
javayhu
69390fed70 refactor(blog) update sitemap for blog pages 2025-06-17 18:17:05 +08:00
javayhu
2cb041beb1 refactor(blog) blog category pages 2025-06-17 18:02:29 +08:00
javayhu
3645cf5773 refactor(blog) optimize inline toc & blog page layout 2025-06-17 17:45:10 +08:00
javayhu
c6ad6d0ad5 refactor(blog) blog page ready with toc 2025-06-17 17:29:15 +08:00
javayhu
53ab869f07 refactor(blog) refactor blog home page 2025-06-17 11:28:20 +08:00
javayhu
e0f408fb07 refactor(blog) update date in mdx files 2025-06-17 09:52:13 +08:00
javayhu
1216732a55 refactor(blog) remove content-collections & add blog source 2025-06-17 09:51:57 +08:00
javayhu
4c6fddf99d refactor(blog) update content about blog posts 2025-06-17 00:08:07 +08:00
javayhu
90d5db88ab refactor(pages) migrate custom pages to using fumadocs 2025-06-16 23:34:22 +08:00
javayhu
af5a3265a6 refactor(changelog) refactor release card component 2025-06-16 22:55:24 +08:00
javayhu
ec8ce54824 refactor(changelog) migrate changelog to use fumadocs 2025-06-16 01:29:02 +08:00
javayhu
f4d8a09ab6 refactor(docs) parse and render docs mdx files by fumadocs 2025-06-16 00:51:36 +08:00
javayhu
3b741b3b98 refactor(docs) remove math and package-install as code block languages 2025-06-16 00:43:27 +08:00
javayhu
b07be5fab4 chore: add fumadocs-mdx 2025-06-15 20:50:03 +08:00
javayhu
a22a5def4d chore: update hyperdrive localConnectionString 2025-06-15 17:44:17 +08:00
javayhu
d190bcb358 chore: add pg to fix error [Better Auth]: INTERNAL_SERVER_ERROR Error: Cannot find module 'cloudflare:sockets' when run pnpm preview 2025-06-15 17:44:00 +08:00
javayhu
7f1fe23407 chore: remove unused types from tsconfig.json 2025-06-15 12:25:06 +08:00
javayhu
05a7de4599 fix: add type annotation for data in GitHubStarsButton component 2025-06-15 12:24:57 +08:00
javayhu
c098300481 chore: update next config & fix build error UnhandledSchemeError 2025-06-15 12:23:10 +08:00
javayhu
e7240db823 fix: fix build error: Module build failed: UnhandledSchemeError: Reading from "cloudflare:sockets" is not handled by plugins (Unhandled scheme).
https://github.com/vercel/next.js/discussions/50177
2025-06-15 12:00:38 +08:00
javayhu
a4390d433b chore: add types to compilerOptions in ts config 2025-06-15 11:44:37 +08:00
javayhu
ae49d06cf4 chore: update db instance & bind hyperdrive 2025-06-15 09:05:27 +08:00
javayhu
6a448825a6 Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-15 08:12:38 +08:00
javayhu
92a87ceb03 chore: update cursor rules 2025-06-15 08:12:15 +08:00
javayhu
3136766a6d chore: add email to footer social config 2025-06-15 00:44:41 +08:00
javayhu
02b5ee1727 chore: optimize feishu notification message 2025-06-15 00:14:14 +08:00
javayhu
b10e27ee29 chore: optimize discord message information 2025-06-14 22:21:39 +08:00
javayhu
ea0c74aa12 fix: z-index 100 is higher than dropdown menu 2025-06-14 19:30:27 +08:00
javayhu
4015cb3143 chore: add 2 custom components 2025-06-14 19:00:21 +08:00
javayhu
2ad6eab666 chore: add 20+ animate ui components 2025-06-14 15:23:22 +08:00
javayhu
c23383fdde chore: add MotionHighlight component from animate-ui 2025-06-14 15:00:55 +08:00
javayhu
e610fe7335 feat: support send message to feishu 2025-06-14 02:09:40 +08:00
javayhu
a7a5a8a6a4 chore: update logo cloud image urls 2025-06-12 01:52:13 +08:00
javayhu
9ffe9af0fa
Merge pull request #48 from MkSaaSHQ/dev/db-refactor
refactor: add getDb function & update all db calls
2025-06-12 01:44:29 +08:00
javayhu
17c7d67743 refactor: add getDb function & update all db calls 2025-06-12 01:40:44 +08:00
javayhu
0684b16278 chore: optimize code for type safe 2025-06-12 00:54:28 +08:00
javayhu
9c120d776d chore: trust images from html.tailus.io 2025-06-12 00:27:16 +08:00
javayhu
a7c56f4a6c chore: optimize db instance init with schema 2025-06-12 00:20:06 +08:00
javayhu
563fc2099a refactor: move db migrations to src/db/migrations folder 2025-06-12 00:04:29 +08:00
javayhu
56df0bed46 fix: optimize og image url in metadata 2025-06-10 22:52:16 +08:00
javayhu
4d60d48212 cf: reset gitignore file same as main branch 2025-06-09 01:25:33 +08:00
javayhu
26a88eb2f0 Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-09 00:58:21 +08:00
javayhu
5b50e62774 chore: move tailark components from /src/app to /src/components (cf deployment error: too many open files) 2025-06-09 00:56:38 +08:00
javayhu
c5d08a9846 Revert "cf: test remove preview pages only"
This reverts commit f5b4ed2859.
2025-06-09 00:42:09 +08:00
javayhu
f5b4ed2859 cf: test remove preview pages only 2025-06-09 00:22:45 +08:00
javayhu
b88aa9c1f5 Revert "cf: test remove docs pages and content only"
This reverts commit 708fac652f.
2025-06-09 00:21:43 +08:00
javayhu
593333c3dd Revert "cf: test remove docs pages and docs+blog content"
This reverts commit c3392320b3.
2025-06-09 00:21:34 +08:00
javayhu
f3b6603db7 Revert "cf: test remove docs and blog pages and content"
This reverts commit 9cb559a48d.
2025-06-09 00:21:28 +08:00
javayhu
9cb559a48d cf: test remove docs and blog pages and content 2025-06-09 00:17:09 +08:00
javayhu
c3392320b3 cf: test remove docs pages and docs+blog content 2025-06-09 00:08:06 +08:00
javayhu
708fac652f cf: test remove docs pages and content only 2025-06-09 00:05:32 +08:00
javayhu
ec124640f1 Revert "cf: test delete blog and docs content only"
This reverts commit 862132d8eb.
2025-06-09 00:03:44 +08:00
javayhu
862132d8eb cf: test delete blog and docs content only 2025-06-08 23:58:11 +08:00
javayhu
bf11c143fe Revert "cf: test remove all blog and docs and preview pages"
This reverts commit 6cfc76d621.
2025-06-08 23:57:16 +08:00
javayhu
6cfc76d621 cf: test remove all blog and docs and preview pages 2025-06-08 22:07:33 +08:00
javayhu
d935bcff76 cf: set @opennextjs/cloudflare as devDependencies 2025-06-08 21:49:19 +08:00
javayhu
a727a31e2f cf: revert the og image 2025-06-08 21:41:01 +08:00
javayhu
2cdc520b1d chore: update git ignore for wrangler files 2025-06-08 21:39:38 +08:00
javayhu
81cfc5f6b3 cf: update website name in zh.json 2025-06-08 21:38:14 +08:00
javayhu
8e8291c325 cf: update configs by the opennextjs docs 2025-06-08 21:18:42 +08:00
javayhu
6ff2ea6845 cf: upgrade opennextjs to 1.2.1 2025-06-08 20:04:51 +08:00
javayhu
b6836db12d Merge remote-tracking branch 'origin/main' into cloudflare 2025-06-08 18:25:26 +08:00
javayhu
4631aea7f5 chore: update readme 2025-06-08 18:09:32 +08:00
javayhu
8b3e9ecfe1 Merge remote-tracking branch 'origin/main' into credits 2025-06-08 11:13:00 +08:00
javayhu
00dcd7ce61 chore: optimize navbar menu link active status 2025-06-07 23:36:18 +08:00
javayhu
9b7c1387f9 chore: make navbar z-index higher 2025-06-07 18:15:42 +08:00
javayhu
da69bb7d20 chore: update readme 2025-05-31 13:45:47 +08:00
javayhu
7edc627505 chore: set enableDiscordWidget to false as default 2025-05-31 13:43:48 +08:00
javayhu
34b7c1c74f custom: support promotekit affiliate 2025-05-31 12:13:28 +08:00
javayhu
80763b4efb chore: add generateStaticParams to blog post page 2025-05-31 09:21:40 +08:00
javayhu
6a4f0575c4 fix: fix cr bugs from coderabbit 2025-05-31 09:19:56 +08:00
javayhu
d391d35e26 fix: remove generateStaticParams in blog post page 2025-05-31 00:47:29 +08:00
javayhu
5f6e75fe93 Merge branch 'main' of github.com:MkSaaSHQ/mksaas-template 2025-05-31 00:42:54 +08:00
javayhu
5fae666b70 fix: set dynamic to force-static in blog post page 2025-05-31 00:42:14 +08:00
javayhu
377e46a31f
Merge pull request #43 from MkSaaSHQ/blog-refactor
refactor: update blog url path & components & sitemap
2025-05-31 00:11:41 +08:00
javayhu
c23fdee88f chore: optimize blog page metadata 2025-05-30 18:27:22 +08:00
javayhu
3a8acc5ef4 chore: update credit related functions (2) 2025-05-29 01:21:04 +08:00
javayhu
4374f118b4 chore: update credits related functions (2) 2025-05-28 00:54:27 +08:00
javayhu
443f01769c Merge branch 'main' into credits 2025-05-27 23:47:41 +08:00
javayhu
ac320b21f4 chore: update credits related functions 2025-05-23 00:38:30 +08:00
javayhu
9e0bd57ecc feat: support credits 2025-05-22 00:30:10 +08:00
javayhu
5f435b9614 chore: add mcp tool of context7 2025-05-18 17:36:30 +08:00
javayhu
9b03f6201f chore: show discord widget in homepage only 2025-05-18 16:55:37 +08:00
javayhu
111f00adaa chore: update @opennextjs/cloudflare version 2025-05-18 00:39:49 +08:00
javayhu
002d2090c2 Merge remote-tracking branch 'origin/main' into cloudflare 2025-05-17 23:40:51 +08:00
javayhu
c3913dbc88 chore: update open graph image 2025-05-13 01:43:25 +08:00
javayhu
9b68e3095e Merge remote-tracking branch 'origin/main' into cloudflare 2025-05-11 22:19:58 +08:00
javayhu
2fb627a6e9 chore: remove package-lock file 2025-05-08 00:07:35 +08:00
javayhu
f11e37374b Merge remote-tracking branch 'origin/main' into cloudflare 2025-05-08 00:03:57 +08:00
javayhu
3560616b52 Revert "chore: remove some doc files"
This reverts commit dd95dece87.
2025-05-08 00:03:34 +08:00
javayhu
80219fa10b Revert "fix: try fix build error, Error: EMFILE: too many open files"
This reverts commit a62abbf399.
2025-05-08 00:03:21 +08:00
javayhu
a62abbf399 fix: try fix build error, Error: EMFILE: too many open files
https://dash.cloudflare.com/b84ee5b2c0cdee9b0371c366945b0ab1/workers/services/view/mksaas-template/production/builds/7204761e-a2d1-490d-8d9d-b77a3985c1de
2025-05-07 00:06:49 +08:00
javayhu
dd95dece87 chore: remove some doc files 2025-05-06 23:52:32 +08:00
javayhu
c938122f7e chore: update package.json 2025-05-06 23:52:29 +08:00
javayhu
3887da26d0 chore: update wrangler config & add minify 2025-05-06 23:42:16 +08:00
javayhu
7af193f770 Merge remote-tracking branch 'origin/main' into cloudflare 2025-05-06 23:32:54 +08:00
javayhu
d6093394d8 chore: update cf scripts 2025-05-06 00:51:39 +08:00
javayhu
f1537e305a fix: build error & try add NEXT_PRIVATE_MAX_WORKER_THREADS=2 2025-05-06 00:42:31 +08:00
javayhu
1847ef4363 custom: update metadata name 2025-05-06 00:03:19 +08:00
javayhu
0fd695c8bc fix: build 2025-05-05 23:25:00 +08:00
javayhu
ae083a7992 feat: support cloudflare by diverce 2025-05-05 23:15:54 +08:00
448 changed files with 37417 additions and 6205 deletions

View File

@ -7,7 +7,7 @@ alwaysApply: false
## Database (Drizzle ORM)
- Schema definitions in `src/db/schema.ts`
- Migrations in `drizzle/`
- Migrations in `src/db/migrations`
- Use `db:generate` to create new migration files based on schema changes
- Use `db:migrate` to apply pending migrations to the database
- Use `db:push` to sync schema changes directly to the database (development only)

View File

@ -19,6 +19,7 @@ alwaysApply: false
- `src/payment/`: Payment integration
- `src/analytics/`: Analytics and tracking
- `src/storage/`: File storage integration
- `src/notification/`: Sending Notifications
## Configuration Files
- `next.config.ts`: Next.js configuration
@ -29,7 +30,7 @@ alwaysApply: false
## Content Management
- `content/`: MDX content files
- `content-collections.ts`: Content collection configuration
- `source.config.ts`: Fumadocs source configuration
## Environment
- `env.example`: Environment variables template

16
.dockerignore Normal file
View File

@ -0,0 +1,16 @@
.cursor
.claude
.conductor
.kiro
.github
.next
.open-next
.source
.vscode
.git
.wrangler
.dockerignore
node_modules
**/node_modules
Dockerfile
LICENSE

41
.gitattributes vendored Normal file
View File

@ -0,0 +1,41 @@
# Set default behavior to automatically normalize line endings
* text=auto
# Force LF line endings for text files
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.md text eol=lf
*.mdx text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.xml text eol=lf
*.txt text eol=lf
*.sh text eol=lf
# Ensure these files are always treated as text and get LF line endings
.gitignore text eol=lf
.gitattributes text eol=lf
.editorconfig text eol=lf
*.config.js text eol=lf
*.config.ts text eol=lf
# Binary files should be left untouched
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.pdf binary
*.zip binary
*.tar.gz binary

20
.gitignore vendored
View File

@ -30,12 +30,23 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
certificates
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# claude code
.claude
# conductor
.conductor
# kiro
.kiro
# typescript
*.tsbuildinfo
next-env.d.ts
@ -43,5 +54,14 @@ next-env.d.ts
# content collections
.content-collections
# fumadocs
.source
# OpenNext build output
.open-next
# wrangler files
.wrangler
.dev.vars
.dev.vars*
!.dev.vars.example

View File

@ -4,6 +4,7 @@
"bradlc.vscode-tailwindcss",
"Lokalise.i18n-ally",
"unifiedjs.vscode-mdx",
"eamodio.gitlens"
"eamodio.gitlens",
"editorconfig.editorconfig"
]
}

12
.vscode/settings.json vendored
View File

@ -22,6 +22,14 @@
"search.exclude": {
"**/node_modules": true,
".next": true,
".content-collections": true,
".source": true,
".wrangler": true,
".open-next": true,
".vscode": true,
".cursor": true,
".claude": true,
".conductor": true,
".kiro": true,
".github": true
}
}
}

109
CLAUDE.md Normal file
View File

@ -0,0 +1,109 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Core Development
- `pnpm dev` - Start development server with content collections
- `pnpm build` - Build the application and content collections
- `pnpm start` - Start production server
- `pnpm lint` - Run Biome linter (use for code quality checks)
- `pnpm format` - Format code with Biome
### Database Operations (Drizzle ORM)
- `pnpm db:generate` - Generate new migration files based on schema changes
- `pnpm db:migrate` - Apply pending migrations to the database
- `pnpm db:push` - Sync schema changes directly to the database (development only)
- `pnpm db:studio` - Open Drizzle Studio for database inspection and management
### Content and Email
- `pnpm content` - Process MDX content collections
- `pnpm email` - Start email template development server on port 3333
## Project Architecture
This is a Next.js 15 full-stack SaaS application with the following key architectural components:
### Core Stack
- **Framework**: Next.js 15 with App Router
- **Database**: PostgreSQL with Drizzle ORM
- **Authentication**: Better Auth with social providers (Google, GitHub)
- **Payments**: Stripe integration with subscription and one-time payments
- **UI**: Radix UI components with TailwindCSS
- **State Management**: Zustand for client-side state
- **Internationalization**: next-intl with English and Chinese locales
- **Content**: Fumadocs for documentation and MDX for content
- **Code Quality**: Biome for formatting and linting
### Key Directory Structure
- `src/app/` - Next.js app router with internationalized routing
- `src/components/` - Reusable React components organized by feature
- `src/lib/` - Utility functions and shared code
- `src/db/` - Database schema and migrations
- `src/actions/` - Server actions for API operations
- `src/stores/` - Zustand state management
- `src/hooks/` - Custom React hooks
- `src/config/` - Application configuration files
- `src/i18n/` - Internationalization setup
- `src/mail/` - Email templates and mail functionality
- `src/payment/` - Stripe payment integration
- `src/credits/` - Credit system implementation
- `content/` - MDX content files for docs and blog
- `messages/` - Translation files (en.json, zh.json) for internationalization
### Authentication & User Management
- Uses Better Auth with PostgreSQL adapter
- Supports email/password and social login (Google, GitHub)
- Includes user management, email verification, and password reset
- Admin plugin for user management and banning
- Automatic newsletter subscription on user creation
### Payment System
- Stripe integration for subscriptions and one-time payments
- Three pricing tiers: Free, Pro (monthly/yearly), and Lifetime
- Credit system with packages for pay-per-use features
- Customer portal for subscription management
### Feature Modules
- **Blog**: MDX-based blog with pagination and categories
- **Docs**: Fumadocs-powered documentation
- **AI Features**: Image generation with multiple providers (OpenAI, Replicate, etc.)
- **Newsletter**: Email subscription system
- **Analytics**: Multiple analytics providers support
- **Storage**: S3 integration for file uploads
### Development Workflow
1. Use TypeScript for all new code
2. Follow Biome formatting rules (single quotes, trailing commas)
3. Write server actions in `src/actions/`
4. Use Zustand for client-side state management
5. Implement database changes through Drizzle migrations
6. Use Radix UI components for consistent UI
7. Follow the established directory structure
8. Use proper error handling with error.tsx and not-found.tsx
9. Leverage Next.js 15 features like Server Actions
10. Use `next-safe-action` for secure form submissions
### Configuration
- Main config in `src/config/website.tsx`
- Environment variables template in `env.example`
- Database config in `drizzle.config.ts`
- Biome config in `biome.json` with specific ignore patterns
- TypeScript config with path aliases (@/* for src/*)
### Testing and Quality
- Use Biome for linting and formatting
- TypeScript for type safety
- Environment variables for configuration
- Proper error boundaries and not-found pages
- Zod for runtime validation
## Important Notes
- The project uses pnpm as the package manager
- Database schema is in `src/db/schema.ts` with auth, payment, and credit tables
- Email templates are in `src/mail/templates/`
- The app supports both light and dark themes
- Content is managed through MDX files in the `content/` directory
- The project includes comprehensive internationalization support

62
Dockerfile Normal file
View File

@ -0,0 +1,62 @@
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json pnpm-lock.yaml* ./
# Copy config files needed for fumadocs-mdx postinstall
COPY source.config.ts ./
COPY content ./content
RUN npm install -g pnpm && pnpm i --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN npm install -g pnpm \
&& DOCKER_BUILD=true pnpm build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

View File

@ -19,28 +19,27 @@ If you found anything that could be improved, please let me know.
- 🔥 website: [mksaas.com](https://mksaas.com)
- 🌐 demo: [demo.mksaas.com](https://demo.mksaas.com)
- 📚 documentation: [mksaas.com/docs](https://mksaas.com/docs)
- 🗓️ roadmap: [mksaas project](https://mksaas.link/roadmap)
- 🗓️ roadmap: [mksaas roadmap](https://mksaas.link/roadmap)
- 👨‍💻 discord: [mksaas.link/discord](https://mksaas.link/discord)
- 📹 video (WIP): [mksaas.link/youtube](https://mksaas.link/youtube)
- 📹 video: [mksaas.link/youtube](https://mksaas.link/youtube)
## Repositories
By default, you should have access to all four repositories. If you find that youre unable to access any of them, please dont hesitate to reach out to me, and Ill assist you in resolving the issue.
By default, you should have access to all 5 repositories. If you find that youre unable to access any of them, please dont hesitate to reach out to me, and Ill assist you in resolving the issue.
- [MkSaaSHQ/mksaas-template](https://github.com/MkSaaSHQ/mksaas-template): https://demo.mksaas.com (ready)
- [MkSaaSHQ/mksaas-blog](https://github.com/MkSaaSHQ/mksaas-blog): https://mksaas.me (ready)
- [MkSaaSHQ/mksaas-app](https://github.com/MkSaaSHQ/mksaas-app): https://mksaas.app (WIP)
- [MkSaaSHQ/mksaas-haitang](https://github.com/MkSaaSHQ/mksaas-haitang): https://haitang.app (WIP)
- [mksaas-template (ready)](https://github.com/MkSaaSHQ/mksaas-template): https://demo.mksaas.com
- [mksaas-blog (ready)](https://github.com/MkSaaSHQ/mksaas-blog): https://mksaas.me
- [mksaas-haitang (ready)](https://github.com/MkSaaSHQ/mksaas-haitang): https://haitang.app
- [mksaas-outfit (ready)](https://github.com/MkSaaSHQ/mksaas-outfit)
- [mksaas-app (WIP)](https://github.com/MkSaaSHQ/mksaas-app): https://mksaas.app
## Notice
> If you have any questions, please [submit an issue](https://github.com/MkSaaSHQ/mksaas-template/issues/new), or contact me at [support@mksaas.com](mailto:support@mksaas.com).
> If you have any feature requests or questions or ideas to share, please [submit it in the discussions](https://github.com/MkSaaSHQ/mksaas-template/discussions).
> If you have any questions, please [submit an issue](https://github.com/MkSaaSHQ/mksaas-template/issues/new), or contact me at [support@mksaas.com](mailto:support@mksaas.com), or join our [discord community](https://mksaas.link/discord) and ask for help there.
> If you want to receive notifications whenever code changes, please click `Watch` button in the top right.
> When submitting any content to the issues or discussions of the repository, please use **English** as the main Language, so that everyone can read it and help you, thank you for your supports.
> When submitting any content to the issues of the repository, please use **English** as the main Language, so that everyone can read it and help you, thank you for your supports.
## License

View File

@ -9,22 +9,28 @@
"ignoreUnknown": true,
"ignore": [
".next/**",
".open-next/**",
".wrangler/**",
".cursor/**",
".claude/**",
".kiro/**",
".conductor/**",
".vscode/**",
".content-collections/**",
".source/**",
"node_modules/**",
"dist/**",
"build/**",
"drizzle/**",
"src/db/**",
"tailwind.config.ts",
"src/components/ui/*.tsx",
"src/components/magicui/*.tsx",
"src/components/animate-ui/*.tsx",
"src/components/tailark/*.tsx",
"src/components/ai-elements/*.tsx",
"src/app/[[]locale]/preview/**",
"src/db/schema.ts",
"src/payment/types.ts",
"src/types/index.d.ts",
"public/sw.js"
"src/credits/types.ts",
"src/types/index.d.ts"
]
},
"formatter": {
@ -32,7 +38,8 @@
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80,
"formatWithErrors": true
"formatWithErrors": true,
"useEditorconfig": true
},
"organizeImports": {
"enabled": true
@ -67,22 +74,28 @@
},
"ignore": [
".next/**",
".open-next/**",
".wrangler/**",
".cursor/**",
".claude/**",
".conductor/**",
".kiro/**",
".vscode/**",
".content-collections/**",
".source/**",
"node_modules/**",
"dist/**",
"build/**",
"drizzle/**",
"src/db/**",
"tailwind.config.ts",
"src/components/ui/*.tsx",
"src/components/magicui/*.tsx",
"src/components/animate-ui/*.tsx",
"src/components/tailark/*.tsx",
"src/components/ai-elements/*.tsx",
"src/app/[[]locale]/preview/**",
"src/db/schema.ts",
"src/payment/types.ts",
"src/types/index.d.ts",
"public/sw.js"
"src/credits/types.ts",
"src/types/index.d.ts"
]
},
"javascript": {

7483
cloudflare-env.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,328 +0,0 @@
import path from 'path';
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
import { defineCollection, defineConfig } from '@content-collections/core';
import {
createDocSchema,
createMetaSchema,
transformMDX,
} from '@fumadocs/content-collections/configuration';
/**
* 1. Content Collections documentation
* https://www.content-collections.dev/docs/quickstart/next
* https://www.content-collections.dev/docs/configuration
* https://www.content-collections.dev/docs/transform#join-collections
*
* 2. Use Content Collections for Fumadocs
* https://fumadocs.dev/docs/headless/content-collections
*/
const docs = defineCollection({
name: 'docs',
directory: 'content/docs',
include: '**/*.mdx',
schema: (z) => ({
...createDocSchema(z),
preview: z.string().optional(),
index: z.boolean().default(false),
}),
transform: transformMDX,
});
const metas = defineCollection({
name: 'meta',
directory: 'content/docs',
include: '**/meta**.json',
parser: 'json',
schema: createMetaSchema,
});
/**
* Blog Author collection
*
* Authors are identified by their slug across all languages
* New format: content/author/authorname.{locale}.mdx
* Example: content/author/mksaas.mdx (default locale) and content/author/mksaas.zh.mdx (Chinese)
*
* For author, slug is slugAsParams
*/
export const authors = defineCollection({
name: 'author',
directory: 'content/author',
include: '**/*.mdx',
schema: (z) => ({
slug: z.string(),
name: z.string(),
avatar: z.string(),
locale: z.string().optional().default(DEFAULT_LOCALE),
}),
transform: async (data, context) => {
// Get the filename from the path
const filePath = data._meta.path;
const fileName = filePath.split(path.sep).pop() || '';
// Extract locale and base from filename
const { locale, base } = extractLocaleAndBase(fileName);
// console.log(`author processed: ${fileName}, locale=${locale}`);
return {
...data,
locale,
};
},
});
/**
* Blog Category collection
*
* Categories are identified by their slug across all languages
* New format: content/category/categoryname.{locale}.mdx
* Example: content/category/tutorial.mdx (default locale) and content/category/tutorial.zh.mdx (Chinese)
*
* For category, slug is slugAsParams
*/
export const categories = defineCollection({
name: 'category',
directory: 'content/category',
include: '**/*.mdx',
schema: (z) => ({
slug: z.string(),
name: z.string(),
description: z.string(),
locale: z.string().optional().default(DEFAULT_LOCALE),
}),
transform: async (data, context) => {
// Get the filename from the path
const filePath = data._meta.path;
const fileName = filePath.split(path.sep).pop() || '';
// Extract locale and base from filename
const { locale, base } = extractLocaleAndBase(fileName);
// console.log(`category processed: ${fileName}, locale=${locale}`);
return {
...data,
locale,
};
},
});
/**
* Blog Post collection
*
* New format: content/blog/post-slug.{locale}.mdx
*
* slug: /blog/first-post, used in URL or sitemap
* slugAsParams: first-post, used in route params
*
* 1. For a blog post at content/blog/first-post.mdx (default locale):
* locale: en
* slug: /blog/first-post
* slugAsParams: first-post
*
* 2. For a blog post at content/blog/first-post.zh.mdx (Chinese locale):
* locale: zh
* slug: /blog/first-post
* slugAsParams: first-post
*/
export const posts = defineCollection({
name: 'post',
directory: 'content/blog',
include: '**/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string(),
image: z.string(),
date: z.string().datetime(),
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
estimatedTime: z.number().optional(), // Reading time in minutes
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
const transformedData = await transformMDX(data, context);
// Get the filename from the path
const filePath = data._meta.path;
const fileName = filePath.split(path.sep).pop() || '';
// Extract locale and base from filename
const { locale, base } = extractLocaleAndBase(fileName);
// console.log(`post processed: ${fileName}, base=${base}, locale=${locale}`);
// Find the author by matching slug and locale
const blogAuthor = context
.documents(authors)
.find((a) => a.slug === data.author && a.locale === locale);
// Find categories by matching slug and locale
const blogCategories = data.categories
.map((categorySlug) => {
const category = context
.documents(categories)
.find((c) => c.slug === categorySlug && c.locale === locale);
return category;
})
.filter(Boolean); // Remove null values
// Create the slug and slugAsParams
const slug = `/blog/${base}`;
const slugAsParams = base;
// Calculate estimated reading time
const wordCount = data.content.split(/\s+/).length;
const wordsPerMinute = 200; // average reading speed: 200 words per minute
const estimatedTime = Math.max(Math.ceil(wordCount / wordsPerMinute), 1);
return {
...data,
locale,
author: blogAuthor,
categories: blogCategories,
slug,
slugAsParams,
estimatedTime,
body: transformedData.body,
toc: transformedData.toc,
};
},
});
/**
* Pages collection for policy pages like privacy-policy, terms-of-service, etc.
*
* New format: content/pages/page-slug.{locale}.mdx
*
* 1. For a page at content/pages/privacy-policy.mdx (default locale):
* locale: en
* slug: /pages/privacy-policy
* slugAsParams: privacy-policy
*
* 2. For a page at content/pages/privacy-policy.zh.mdx (Chinese locale):
* locale: zh
* slug: /pages/privacy-policy
* slugAsParams: privacy-policy
*/
export const pages = defineCollection({
name: 'page',
directory: 'content/pages',
include: '**/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string(),
date: z.string().datetime(),
published: z.boolean().default(true),
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
const transformedData = await transformMDX(data, context);
// Get the filename from the path
const filePath = data._meta.path;
const fileName = filePath.split(path.sep).pop() || '';
// Extract locale and base from filename
const { locale, base } = extractLocaleAndBase(fileName);
// console.log(`page processed: ${fileName}, base=${base}, locale=${locale}`);
// Create the slug and slugAsParams
const slug = `/pages/${base}`;
const slugAsParams = base;
return {
...data,
locale,
slug,
slugAsParams,
body: transformedData.body,
toc: transformedData.toc,
};
},
});
/**
* Releases collection for changelog
*
* New format: content/release/version-slug.{locale}.mdx
*
* 1. For a release at content/release/v1-0-0.mdx (default locale):
* locale: en
* slug: /release/v1-0-0
* slugAsParams: v1-0-0
*
* 2. For a release at content/release/v1-0-0.zh.mdx (Chinese locale):
* locale: zh
* slug: /release/v1-0-0
* slugAsParams: v1-0-0
*/
export const releases = defineCollection({
name: 'release',
directory: 'content/release',
include: '**/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string(),
date: z.string().datetime(),
version: z.string(),
published: z.boolean().default(true),
}),
transform: async (data, context) => {
// Use Fumadocs transformMDX for consistent MDX processing
const transformedData = await transformMDX(data, context);
// Get the filename from the path
const filePath = data._meta.path;
const fileName = filePath.split(path.sep).pop() || '';
// Extract locale and base from filename
const { locale, base } = extractLocaleAndBase(fileName);
// console.log(`release processed: ${fileName}, base=${base}, locale=${locale}`);
// Create the slug and slugAsParams
const slug = `/release/${base}`;
const slugAsParams = base;
return {
...data,
locale,
slug,
slugAsParams,
body: transformedData.body,
toc: transformedData.toc,
};
},
});
/**
* Helper function to extract locale and base name from filename
* Handles filename formats:
* - name -> locale: DEFAULT_LOCALE, base: name
* - name.zh -> locale: zh, base: name
*
* @param fileName Filename without extension (already has .mdx removed)
* @returns Object with locale and base name
*/
function extractLocaleAndBase(fileName: string): {
locale: string;
base: string;
} {
// Split filename into parts
const parts = fileName.split('.');
if (parts.length === 1) {
// Simple filename without locale: xxx
return { locale: DEFAULT_LOCALE, base: parts[0] };
}
if (parts.length === 2 && LOCALES.includes(parts[1])) {
// Filename with locale: xxx.zh
return { locale: parts[1], base: parts[0] };
}
// Unexpected format, use first part as base and default locale
console.warn(`Unexpected filename format: ${fileName}`);
return { locale: DEFAULT_LOCALE, base: parts[0] };
}
export default defineConfig({
collections: [docs, metas, authors, categories, posts, pages, releases],
});

View File

@ -1,5 +1,4 @@
---
slug: fox
name: Fox
avatar: /images/avatars/fox.png
---

View File

@ -1,5 +1,4 @@
---
slug: fox
name: Fox
avatar: /images/avatars/fox.png
---

View File

@ -1,5 +1,4 @@
---
slug: mkdirs
name: Mkdirs
avatar: /images/avatars/mkdirs.png
---

View File

@ -1,5 +1,4 @@
---
slug: mkdirs
name: Mkdirs模板
avatar: /images/avatars/mkdirs.png
---

View File

@ -1,5 +1,4 @@
---
slug: mksaas
name: MkSaaS
avatar: /images/avatars/mksaas.png
---

View File

@ -1,5 +1,4 @@
---
slug: mksaas
name: MkSaaS模板
avatar: /images/avatars/mksaas.png
---

View File

@ -2,7 +2,7 @@
title: Comparisons
description: How is Fumadocs different from other existing frameworks?
image: /images/blog/post-2.png
date: 2025-03-22T12:00:00.000Z
date: "2025-03-22"
published: true
categories: [news, company]
author: fox

View File

@ -2,7 +2,7 @@
title: 对比
description: Fumadocs 与其他现有框架有何不同?
image: /images/blog/post-2.png
date: 2025-03-22T12:00:00.000Z
date: "2025-03-22"
published: true
categories: [news, company]
author: fox
@ -69,4 +69,4 @@ Docusaurus 是一个基于 React.js 的强大框架。它通过插件和自定
您可以通过插件轻松实现许多功能,他们的生态系统确实更大,并由许多贡献者维护。
相比之下Fumadocs 的灵活性允许您自己实现它们,可能需要更长的时间来调整它以达到您的满意度。
相比之下Fumadocs 的灵活性允许您自己实现它们,可能需要更长的时间来调整它以达到您的满意度。

View File

@ -2,7 +2,7 @@
title: Quick Start
description: Getting Started with Fumadocs
image: /images/blog/post-8.png
date: 2025-03-28T12:00:00.000Z
date: "2025-03-28"
published: true
categories: [company, news]
author: mksaas
@ -99,7 +99,7 @@ title: Hello World
Run the app in development mode and see http://localhost:3000/docs.
```package-install
```mdx
npm run dev
```

View File

@ -2,7 +2,7 @@
title: 快速入门
description: Fumadocs 入门指南
image: /images/blog/post-8.png
date: 2025-03-28T12:00:00.000Z
date: "2025-03-28"
published: true
categories: [company, news]
author: mksaas
@ -99,7 +99,7 @@ title: Hello World
在开发模式下运行应用程序并查看 http://localhost:3000/docs。
```package-install
```mdx
npm run dev
```
@ -250,4 +250,4 @@ export const source = loader({
## 了解更多
刚来这里?别担心,我们欢迎您的问题。
刚来这里?别担心,我们欢迎您的问题。

View File

@ -2,7 +2,7 @@
title: Internationalization
description: Support multiple languages in your documentation
image: /images/blog/post-3.png
date: 2025-03-15T12:00:00.000Z
date: "2025-03-15"
published: true
categories: [company, product]
author: mksaas

View File

@ -2,7 +2,7 @@
title: 国际化
description: 在您的文档中支持多种语言
image: /images/blog/post-3.png
date: 2025-03-15T12:00:00.000Z
date: "2025-03-15"
published: true
categories: [company, product]
author: mksaas
@ -224,4 +224,4 @@ return <Link href={`/${lang}/another-page`}>This is a link</Link>;
import { DynamicLink } from 'fumadocs-core/dynamic-link';
<DynamicLink href="/[lang]/another-page">This is a link</DynamicLink>
```
```

View File

@ -2,7 +2,7 @@
title: Manual Installation
description: Create a new fumadocs project from scratch.
image: /images/blog/post-4.png
date: 2025-03-14T12:00:00.000Z
date: "2025-03-14"
published: true
categories: [company, product]
author: mkdirs
@ -14,7 +14,7 @@ author: mkdirs
Create a new Next.js application with `create-next-app`, and install required packages.
```package-install
```mdx
fumadocs-ui fumadocs-core
```

View File

@ -2,7 +2,7 @@
title: 手动安装
description: 从零开始创建一个新的 Fumadocs 项目
image: /images/blog/post-4.png
date: 2025-03-14T12:00:00.000Z
date: "2025-03-14"
published: true
categories: [company, product]
author: mkdirs
@ -14,7 +14,7 @@ author: mkdirs
使用 `create-next-app` 创建一个新的 Next.js 应用程序,并安装所需的包。
```package-install
```mdx
fumadocs-ui fumadocs-core
```
@ -193,4 +193,4 @@ WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* source.config.ts ./
```
这确保 Fumadocs MDX 在构建期间可以访问您的配置文件。
这确保 Fumadocs MDX 在构建期间可以访问您的配置文件。

View File

@ -2,7 +2,7 @@
title: Markdown
description: How to write documents
image: /images/blog/post-5.png
date: 2025-03-05T12:00:00.000Z
date: "2025-03-05"
published: true
categories: [news, company]
author: mkdirs
@ -353,12 +353,12 @@ Some optional plugins you can enable.
Write math equations with TeX.
````md
```math
```mdx
f(x) = x * e^{2 pi i \xi x}
```
````
```math
```mdx
f(x) = x * e^{2 pi i \xi x}
```
@ -369,12 +369,12 @@ To enable, see [Math Integration](/docs/math).
Generate code blocks for installing packages via package managers (JS/Node.js).
````md
```package-install
```mdx
npm i next -D
```
````
```package-install
```mdx
npm i next -D
```

View File

@ -2,7 +2,7 @@
title: Markdown
description: 如何撰写文档
image: /images/blog/post-5.png
date: 2025-03-05T12:00:00.000Z
date: "2025-03-05"
published: true
categories: [news, company]
author: mkdirs
@ -251,7 +251,7 @@ console.log('Hello World');
```
````
### 高亮行
### 高亮行
````md
```tsx

56
content/blog/premium.mdx Normal file
View File

@ -0,0 +1,56 @@
---
title: "Premium Blog Post"
description: "This blog post is a test for premium content."
date: "2025-08-30"
published: true
premium: true
categories: ["product"]
author: "fox"
image: "/images/blog/post-7.png"
---
This blog post is a test for premium content.
You can read this part of the blog post if you are not a premium user.
But for the rest of the blog post, you need to be logged in as a premium user.
You can click the "Sign In" button to sign in as a user with free plan.
Then you can click the "Upgrade Now" button to upgrade to a premium plan.
<Callout type="warn">
Don't worry, you don't actually pay any cents, because we are in the sandbox environment of Stripe.
</Callout>
You can use the test card number to pay for monthly or yearly PRO plan or LIFETIME plan.
```
Card number: 4242 4242 4242 4242
Exp: 12/34
CVV: 567
```
After that, you can return to the blog post and you can read the rest of the blog post.
For more details, please check out the documentation: [Blog](https://mksaas.com/docs/blog).
Now the rest of the blog post is premium content.
<PremiumContent>
<Callout type="info">
This is the beginning of the premium content part.
</Callout>
This is the premium content part.
You can read this paragraph only if you are a premium user.
Please don't share this blog post with others.
<Callout type="info">
This is the end of the premium content part.
</Callout>
</PremiumContent>

View File

@ -0,0 +1,56 @@
---
title: "测试专用付费文章"
description: "这是一篇测试专用付费文章。"
date: "2025-08-30"
published: true
premium: true
categories: ["product"]
author: "fox"
image: "/images/blog/post-7.png"
---
这是一篇测试专用的付费文章。
如果你不是付费用户,你可以阅读这篇文章的这部分内容。
但如果你想阅读剩下的内容,你需要成为一个付费用户。
你可以点击 "登录" 按钮来以免费用户的身份登录。
然后你可以点击 "立即升级" 按钮来升级到付费计划。
<Callout type="warn">
不用担心,你实际上不需要支付任何费用,因为我们处于 Stripe 的沙盒环境中。
</Callout>
你可以使用测试卡号来支付月度或年度 PRO 计划或终身计划。
```
Card number: 4242 4242 4242 4242
Exp: 12/34
CVV: 567
```
之后,你可以返回这篇博客文章,然后你可以阅读剩下的内容。
更多详情,请参考文档:[博客](https://mksaas.com/docs/blog)。
现在剩下的内容是付费内容。
<PremiumContent>
<Callout type="info">
这是付费内容部分的开始。
</Callout>
这是付费内容部分。
你可以阅读这篇内容,只要你是一个付费用户。
请不要分享这篇文章给其他人。
<Callout type="info">
这是付费内容部分的结束。
</Callout>
</PremiumContent>

View File

@ -2,7 +2,7 @@
title: Search
description: Implement document search in your docs
image: /images/blog/post-6.png
date: 2025-02-15T12:00:00.000Z
date: "2025-02-15"
published: true
categories: [company, news]
author: mksaas

View File

@ -2,7 +2,7 @@
title: 搜索
description: 在您的文档中实现文档搜索
image: /images/blog/post-6.png
date: 2025-02-15T12:00:00.000Z
date: "2025-02-15"
published: true
categories: [company, news]
author: mksaas
@ -249,4 +249,4 @@ export default function CustomSearchDialog(props: SharedProps) {
```
1. 将 `endpoint`、`apiKey` 替换为您想要的值。
2. 用您的新组件[替换默认搜索对话框](#replace-search-dialog)。
2. 用您的新组件[替换默认搜索对话框](#replace-search-dialog)。

View File

@ -2,7 +2,7 @@
title: Themes
description: Add Theme to Fumadocs UI
image: /images/blog/post-7.png
date: 2025-01-15T12:00:00.000Z
date: "2025-01-15"
published: true
categories: [product, news]
author: mkdirs

View File

@ -2,7 +2,7 @@
title: 主题
description: 为 Fumadocs UI 添加主题
image: /images/blog/post-7.png
date: 2025-01-15T12:00:00.000Z
date: "2025-01-15"
published: true
categories: [product, news]
author: mkdirs
@ -167,4 +167,4 @@ Tailwind CSS 预设引入了新的颜色和额外的工具,包括 `fd-steps`
```
> 该插件仅与 Fumadocs UI 的 MDX 组件一起工作,它可能与 `@tailwindcss/typography` 冲突。
> 如果您需要使用 `@tailwindcss/typography` 而不是默认插件,请[设置类名选项](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/README.md#changing-the-default-class-name)以避免冲突。
> 如果您需要使用 `@tailwindcss/typography` 而不是默认插件,请[设置类名选项](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/README.md#changing-the-default-class-name)以避免冲突。

View File

@ -2,7 +2,7 @@
title: What is Fumadocs
description: Introducing Fumadocs, a docs framework that you can break.
image: /images/blog/post-1.png
date: 2025-04-01T12:00:00.000Z
date: "2025-04-01"
published: true
categories: [company, product]
author: fox

View File

@ -2,7 +2,7 @@
title: 什么是 Fumadocs
description: 介绍 Fumadocs一个可以打破常规的文档框架
image: /images/blog/post-1.png
date: 2025-04-01T12:00:00.000Z
date: "2025-04-01"
published: true
categories: [company, product]
author: fox
@ -57,4 +57,4 @@ Fumadocs 为 Next.js 提供了额外的工具,包括语法高亮、文档搜
Fumadocs 由 Fuma 和许多贡献者维护,关注代码库的可维护性。
虽然我们不打算提供人们想要的每一项功能,但我们更专注于使基本功能完美且维护良好。
您也可以通过贡献来帮助 Fumadocs 变得更加有用!
您也可以通过贡献来帮助 Fumadocs 变得更加有用!

View File

@ -1,5 +1,4 @@
---
slug: company
name: Company
description: Company news and updates
---

View File

@ -1,5 +1,4 @@
---
slug: company
name: 公司
description: 公司新闻和更新
---

View File

@ -1,5 +1,4 @@
---
slug: news
name: News
description: News and updates about MkSaaS
---

View File

@ -1,5 +1,4 @@
---
slug: news
name: 新闻
description: 最新新闻和更新
---

View File

@ -1,5 +1,4 @@
---
slug: product
name: Product
description: Products and services powered by MkSaaS
---

View File

@ -1,5 +1,4 @@
---
slug: product
name: 产品
description: 产品和服务
---

View File

@ -1,7 +1,7 @@
---
title: "Initial Release"
description: "Our first official release with core features and functionality"
date: "2024-03-01T00:00:00Z"
date: "2024-03-01"
version: "v1.0.0"
published: true
---
@ -27,4 +27,4 @@ We're excited to announce the initial release of our platform with the following
- Fixed issues with user registration flow
- Resolved authentication token expiration handling
- Improved form validation and error messages
- Improved form validation and error messages

View File

@ -1,7 +1,7 @@
---
title: "初始版本"
description: "我们的第一个正式版本,包含核心功能"
date: "2024-03-01T00:00:00Z"
date: "2024-03-01"
version: "v1.0.0"
published: true
---
@ -27,4 +27,4 @@ published: true
- 修复了用户注册流程中的问题
- 解决了身份验证令牌过期处理
- 改进了表单验证和错误消息
- 改进了表单验证和错误消息

View File

@ -1,7 +1,7 @@
---
title: "Feature Update"
description: "New features and improvements to enhance your experience"
date: "2024-03-15T00:00:00Z"
date: "2024-03-15"
version: "v1.1.0"
published: true
---
@ -27,4 +27,4 @@ We've added several new features to improve your experience:
- Fixed issue with project duplication
- Resolved calendar sync problems
- Fixed data import validation errors
- Improved error handling for API requests
- Improved error handling for API requests

View File

@ -1,7 +1,7 @@
---
title: "功能更新"
description: "新功能和改进,提升您的使用体验"
date: "2024-03-15T00:00:00Z"
date: "2024-03-15"
version: "v1.1.0"
published: true
---
@ -27,4 +27,4 @@ published: true
- 修复了项目复制问题
- 解决了日历同步问题
- 修复了数据导入验证错误
- 改进了API请求的错误处理
- 改进了API请求的错误处理

View File

@ -1,7 +1,7 @@
---
title: "AI Integration"
description: "Introducing AI-powered features to boost productivity"
date: "2024-03-30T00:00:00Z"
date: "2024-03-30"
version: "v1.2.0"
published: true
---
@ -34,4 +34,4 @@ We're thrilled to introduce our new AI capabilities:
- Fixed issues with file uploads on certain browsers
- Resolved synchronization issues between devices
- Improved error handling for third-party integrations
- Fixed accessibility issues in the dashboard
- Fixed accessibility issues in the dashboard

View File

@ -1,7 +1,7 @@
---
title: "AI集成"
description: "引入AI驱动的功能提高生产力"
date: "2024-03-30T00:00:00Z"
date: "2024-03-30"
version: "v1.2.0"
published: true
---
@ -34,4 +34,4 @@ published: true
- 修复了某些浏览器上文件上传的问题
- 解决了设备之间的同步问题
- 改进了第三方集成的错误处理
- 修复了仪表板中的可访问性问题
- 修复了仪表板中的可访问性问题

View File

@ -2,4 +2,4 @@
title: 组件
description: 改进文档的额外组件
index: true
---
---

View File

@ -46,6 +46,6 @@ Since the design system is built on Tailwind CSS, you can customise it [with CSS
If none of them suits you, Fumadocs CLI is a tool to install Fumadocs UI components and layouts to your codebase, similar to Shadcn UI. Allowing you to fully customise Fumadocs UI:
```package-install
```mdx
npx fumadocs add
```

View File

@ -46,6 +46,6 @@ Fumadocs UI 还提供了样式化组件,用于交互式示例以增强您的
如果这些都不适合您Fumadocs CLI 是一个工具,可以将 Fumadocs UI 组件和布局安装到您的代码库中,类似于 Shadcn UI。允许您完全自定义 Fumadocs UI
```package-install
```mdx
npx fumadocs add
```
```

View File

@ -95,7 +95,7 @@ title: Hello World
Run the app in development mode and see http://localhost:3000/docs.
```package-install
```mdx
npm run dev
```

View File

@ -95,7 +95,7 @@ title: Hello World
在开发模式下运行应用程序并查看 http://localhost:3000/docs。
```package-install
```mdx
npm run dev
```
@ -250,4 +250,4 @@ export const source = loader({
## 了解更多
刚来这里?别担心,我们欢迎您的问题。
刚来这里?别担心,我们欢迎您的问题。

View File

@ -9,7 +9,7 @@ description: Create a new fumadocs project from scratch.
Create a new Next.js application with `create-next-app`, and install required packages.
```package-install
```mdx
fumadocs-ui fumadocs-core
```

View File

@ -9,7 +9,7 @@ description: 从零开始创建一个新的 Fumadocs 项目
使用 `create-next-app` 创建一个新的 Next.js 应用程序,并安装所需的包。
```package-install
```mdx
fumadocs-ui fumadocs-core
```
@ -188,4 +188,4 @@ WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* source.config.ts ./
```
这确保 Fumadocs MDX 在构建期间可以访问您的配置文件。
这确保 Fumadocs MDX 在构建期间可以访问您的配置文件。

View File

@ -348,12 +348,12 @@ Some optional plugins you can enable.
Write math equations with TeX.
````md
```math
```mdx
f(x) = x * e^{2 pi i \xi x}
```
````
```math
```mdx
f(x) = x * e^{2 pi i \xi x}
```
@ -364,12 +364,12 @@ To enable, see [Math Integration](/docs/math).
Generate code blocks for installing packages via package managers (JS/Node.js).
````md
```package-install
```mdx
npm i next -D
```
````
```package-install
```mdx
npm i next -D
```

View File

@ -2,6 +2,7 @@
title: What is Fumadocs
description: Introducing Fumadocs, a docs framework that you can break.
icon: CircleHelp
premium: true
---
Fumadocs was created because I wanted a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**.
@ -18,6 +19,8 @@ You are still using features of Next.js App Router, like **Static Site Generatio
**Opinionated on UI:** The only thing Fumadocs UI (the default theme) offers is **User Interface**. The UI is opinionated for bringing better mobile responsiveness and user experience.
Instead, we use a much more flexible approach inspired by Shadcn UI — [Fumadocs CLI](/docs/cli), so we can iterate our design quick, and welcome for more feedback about the UI.
<PremiumContent>
## Why Fumadocs
Fumadocs is designed with flexibility in mind.
@ -56,3 +59,5 @@ docs easier, with less boilerplate.
Fumadocs is maintained by Fuma and many contributors, with care on the maintainability of codebase.
While we don't aim to offer every functionality people wanted, we're more focused on making basic features perfect and well-maintained.
You can also help Fumadocs to be more useful by contributing!
</PremiumContent>

View File

@ -2,6 +2,7 @@
title: 什么是 Fumadocs
description: 介绍 Fumadocs一个可以打破常规的文档框架
icon: CircleHelp
premium: true
---
Fumadocs 的创建是因为我想要一种更加可定制化的文档构建体验,一个不固执己见的文档框架,**一个你可以"打破"的"框架"**。
@ -18,6 +19,8 @@ Fumadocs 的创建是因为我想要一种更加可定制化的文档构建体
**对 UI 有自己的看法:** Fumadocs UI默认主题提供的唯一东西是**用户界面**。UI 的设计理念是提供更好的移动响应性和用户体验。
相反,我们使用受 Shadcn UI 启发的更灵活的方法 — [Fumadocs CLI](/docs/cli),这样我们可以快速迭代设计,并欢迎更多关于 UI 的反馈。
<PremiumContent>
## 为什么选择 Fumadocs
Fumadocs 的设计考虑了灵活性。
@ -53,4 +56,6 @@ Fumadocs 为 Next.js 提供了额外的工具,包括语法高亮、文档搜
Fumadocs 由 Fuma 和许多贡献者维护,关注代码库的可维护性。
虽然我们不打算提供人们想要的每一项功能,但我们更专注于使基本功能完美且维护良好。
您也可以通过贡献来帮助 Fumadocs 变得更加有用!
您也可以通过贡献来帮助 Fumadocs 变得更加有用!
</PremiumContent>

View File

@ -1,7 +1,7 @@
---
title: Cookie Policy
description: How we use cookies and similar technologies on our website
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

View File

@ -1,7 +1,7 @@
---
title: Cookie 政策
description: 我们如何在网站上使用 Cookie 和类似技术
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

View File

@ -1,7 +1,7 @@
---
title: Privacy Policy
description: Our commitment to protecting your privacy and personal data
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

View File

@ -1,7 +1,7 @@
---
title: 隐私政策
description: 我们致力于保护您的隐私和个人数据
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

View File

@ -1,7 +1,7 @@
---
title: Terms of Service
description: The terms and conditions governing the use of our services
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

View File

@ -1,7 +1,7 @@
---
title: 服务条款
description: 管理我们服务使用的条款和条件
date: 2025-03-10T00:00:00.000Z
date: "2025-03-10"
published: true
---

1
dev.vars.example Normal file
View File

@ -0,0 +1 @@
NEXTJS_ENV=development

View File

@ -5,7 +5,7 @@ import { defineConfig } from 'drizzle-kit';
* https://orm.drizzle.team/docs/get-started/neon-new#step-5---setup-drizzle-config-file
*/
export default defineConfig({
out: './drizzle',
out: './src/db/migrations',
schema: './src/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {

View File

@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1744304844165,
"tag": "0000_fine_sir_ram",
"breakpoints": true
}
]
}

View File

@ -8,13 +8,13 @@ NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# -----------------------------------------------------------------------------
# Database
# https://mksaas.com/docs/database#setup
# https://mksaas.com/docs/database
# -----------------------------------------------------------------------------
DATABASE_URL=""
# -----------------------------------------------------------------------------
# Better Auth
# https://mksaas.com/docs/auth#setup
# https://mksaas.com/docs/auth
# Generate a random string for the secret key using `openssl rand -base64 32`
# -----------------------------------------------------------------------------
BETTER_AUTH_SECRET=""
@ -39,8 +39,8 @@ GOOGLE_CLIENT_SECRET=""
# -----------------------------------------------------------------------------
# Email / Newsletter (Resend)
# https://mksaas.com/docs/email#setup
# https://mksaas.com/docs/newsletter#setup
# https://mksaas.com/docs/email
# https://mksaas.com/docs/newsletter
# Get API key and audience id from https://resend.com
# -----------------------------------------------------------------------------
RESEND_API_KEY=""
@ -48,7 +48,7 @@ RESEND_AUDIENCE_ID=""
# -----------------------------------------------------------------------------
# Storage (Cloudflare R2 or S3-compatible service of your choice)
# https://mksaas.com/docs/storage#setup
# https://mksaas.com/docs/storage
# Cloudflare R2: https://www.cloudflare.com/developer-platform/products/r2
# -----------------------------------------------------------------------------
STORAGE_REGION="auto"
@ -56,12 +56,11 @@ STORAGE_BUCKET_NAME=""
STORAGE_ACCESS_KEY_ID=""
STORAGE_SECRET_ACCESS_KEY=""
STORAGE_ENDPOINT=""
STORAGE_FORCE_PATH_STYLE="false"
STORAGE_PUBLIC_URL=""
# -----------------------------------------------------------------------------
# Payment (Stripe)
# https://mksaas.com/docs/payment#setup
# https://mksaas.com/docs/payment
# Get Stripe key and secret from https://dashboard.stripe.com
# -----------------------------------------------------------------------------
STRIPE_SECRET_KEY=""
@ -72,18 +71,29 @@ NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY=""
NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY=""
# Lifetime plan - one-time payment
NEXT_PUBLIC_STRIPE_PRICE_LIFETIME=""
# Credit package - basic
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC=""
# Credit package - standard
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD=""
# Credit package - premium
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM=""
# Credit package - enterprise
NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE=""
# -----------------------------------------------------------------------------
# Configurations
# -----------------------------------------------------------------------------
# Disable image optimization, check out next.config.ts for more details
# -----------------------------------------------------------------------------
DISABLE_IMAGE_OPTIMIZATION=false
# -----------------------------------------------------------------------------
# Run this website as demo website, in most cases, you should set this to false
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DEMO_WEBSITE=false
# -----------------------------------------------------------------------------
# Analytics
# https://mksaas.com/docs/analytics#setup
# https://mksaas.com/docs/analytics
# -----------------------------------------------------------------------------
# Google Analytics (https://analytics.google.com)
# https://mksaas.com/docs/analytics#google
@ -120,18 +130,68 @@ NEXT_PUBLIC_SELINE_TOKEN=""
# DataFast Analytics (https://datafa.st)
# https://mksaas.com/docs/analytics#datafast
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DATAFAST_ANALYTICS_ID=""
NEXT_PUBLIC_DATAFAST_ANALYTICS_DOMAIN=""
NEXT_PUBLIC_DATAFAST_WEBSITE_ID=""
NEXT_PUBLIC_DATAFAST_DOMAIN=""
# -----------------------------------------------------------------------------
# Discord
# Notification (Discord)
# -----------------------------------------------------------------------------
NEXT_PUBLIC_DISCORD_WIDGET_SERVER_ID=""
NEXT_PUBLIC_DISCORD_WIDGET_CHANNEL_ID=""
DISCORD_WEBHOOK_URL=""
# -----------------------------------------------------------------------------
# Notification (Feishu)
# -----------------------------------------------------------------------------
FEISHU_WEBHOOK_URL=""
# -----------------------------------------------------------------------------
# Affiliate (Affonso)
# Affiliate
# https://mksaas.com/docs/affiliate
# -----------------------------------------------------------------------------
# Affonso
# https://affonso.com/
# -----------------------------------------------------------------------------
NEXT_PUBLIC_AFFILIATE_AFFONSO_ID=""
# -----------------------------------------------------------------------------
# PromoteKit
# https://www.promotekit.com/
# -----------------------------------------------------------------------------
NEXT_PUBLIC_AFFILIATE_PROMOTEKIT_ID=""
# -----------------------------------------------------------------------------
# Captcha (Cloudflare Turnstile)
# https://mksaas.com/docs/captcha
# -----------------------------------------------------------------------------
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
# -----------------------------------------------------------------------------
# Crisp
# https://mksaas.com/docs/chat
# -----------------------------------------------------------------------------
NEXT_PUBLIC_CRISP_WEBSITE_ID=""
# -----------------------------------------------------------------------------
# Cron Jobs
# https://mksaas.com/docs/cronjobs
# -----------------------------------------------------------------------------
CRON_JOBS_USERNAME=""
CRON_JOBS_PASSWORD=""
# -----------------------------------------------------------------------------
# AI
# https://mksaas.com/docs/ai
# -----------------------------------------------------------------------------
AI_GATEWAY_API_KEY=""
FAL_API_KEY=""
FIREWORKS_API_KEY=""
OPENAI_API_KEY=""
REPLICATE_API_TOKEN=""
GOOGLE_GENERATIVE_AI_API_KEY=""
DEEPSEEK_API_KEY=""
OPENROUTER_API_KEY=""
# -----------------------------------------------------------------------------
# Web Content Analyzer (Firecrawl)
# https://firecrawl.dev/
# -----------------------------------------------------------------------------
FIRECRAWL_API_KEY=""

View File

@ -5,6 +5,7 @@
"description": "MkSaaS is the best AI SaaS boilerplate. Make AI SaaS in days, simply and effortlessly"
},
"Common": {
"premium": "Premium",
"login": "Log in",
"logout": "Log out",
"signUp": "Sign up",
@ -28,7 +29,21 @@
"save": "Save",
"loading": "Loading...",
"cancel": "Cancel",
"logoutFailed": "Failed to log out"
"logoutFailed": "Failed to log out",
"table": {
"totalRecords": "Total {count} records",
"noResults": "No results",
"loading": "Loading...",
"columns": "Columns",
"rowsPerPage": "Rows per page",
"page": "Page",
"firstPage": "First Page",
"lastPage": "Last Page",
"nextPage": "Next Page",
"previousPage": "Previous Page",
"ascending": "Asc",
"descending": "Desc"
}
},
"PricingPage": {
"title": "Pricing",
@ -99,6 +114,24 @@
}
}
},
"CreditPackages": {
"basic": {
"name": "Basic",
"description": "Basic credits package description"
},
"standard": {
"name": "Standard",
"description": "Standard credits package description"
},
"premium": {
"name": "Premium",
"description": "Premium credits package description"
},
"enterprise": {
"name": "Enterprise",
"description": "Enterprise credits package description"
}
},
"NotFoundPage": {
"title": "404",
"message": "Sorry, the page you are looking for does not exist.",
@ -187,7 +220,9 @@
"hidePassword": "Hide password",
"or": "Or continue with",
"emailRequired": "Please enter your email",
"passwordRequired": "Please enter your password"
"passwordRequired": "Please enter your password",
"captchaInvalid": "Captcha verification failed",
"captchaError": "Captcha verification error"
},
"register": {
"title": "Register",
@ -202,7 +237,9 @@
"hidePassword": "Hide password",
"nameRequired": "Please enter your name",
"emailRequired": "Please enter your email",
"passwordRequired": "Please enter your password"
"passwordRequired": "Please enter your password",
"captchaInvalid": "Captcha verification failed",
"captchaError": "Captcha verification error"
},
"forgotPassword": {
"title": "Forgot Password",
@ -256,8 +293,20 @@
"nextPage": "Next",
"chooseLanguage": "Select language",
"title": "MkSaaS Docs",
"homepage": "Homepage",
"blog": "Blog"
"homepage": "Homepage"
},
"PremiumContent": {
"title": "Unlock Premium Content",
"description": "Subscribe to our Pro plan to access all premium content and exclusive content.",
"upgradeCta": "Upgrade Now",
"benefit1": "All premium content",
"benefit2": "Exclusive content",
"benefit3": "Cancel anytime",
"signIn": "Sign In",
"loginRequired": "Sign in to continue reading",
"loginDescription": "This is premium content. Sign in to your account to access the full content.",
"checkingAccess": "Checking access...",
"loadingContent": "Loading full content..."
},
"Marketing": {
"navbar": {
@ -284,6 +333,10 @@
"title": "AI Image",
"description": "Show how to use AI to generate beautiful images"
},
"chat": {
"title": "AI Chat",
"description": "Show how to use AI to chat with your customers"
},
"video": {
"title": "AI Video",
"description": "Show how to use AI to generate amazing videos"
@ -430,6 +483,7 @@
"avatar": {
"dashboard": "Dashboard",
"billing": "Billing",
"credits": "Credits",
"settings": "Settings"
}
},
@ -455,14 +509,6 @@
"banReason": "Ban Reason",
"banExpires": "Ban Expires"
},
"noResults": "No results",
"firstPage": "First Page",
"lastPage": "Last Page",
"nextPage": "Next Page",
"previousPage": "Previous Page",
"rowsPerPage": "Rows per page",
"page": "Page",
"loading": "Loading...",
"admin": "Admin",
"user": "User",
"email": {
@ -472,8 +518,8 @@
"emailCopied": "Email copied to clipboard",
"banned": "Banned",
"active": "Active",
"joined": "Joined",
"updated": "Updated",
"joined": "Joined at",
"updated": "Updated at",
"ban": {
"reason": "Ban Reason",
"reasonPlaceholder": "Enter the reason for banning this user",
@ -544,7 +590,8 @@
"createCustomerPortalFailed": "Failed to open Stripe customer portal"
},
"price": "Price:",
"nextBillingDate": "Next billing date:",
"periodStartDate": "Period start date:",
"periodEndDate": "Period end date:",
"trialEnds": "Trial ends:",
"freePlanMessage": "You are currently on the free plan with limited features",
"lifetimeMessage": "You have lifetime access to all premium features",
@ -552,7 +599,77 @@
"manageBilling": "Manage Billing",
"upgradePlan": "Upgrade Plan",
"retry": "Retry",
"errorMessage": "Failed to get data"
"errorMessage": "Failed to get data",
"paymentSuccess": "Payment successful"
},
"credits": {
"title": "Credits",
"description": "Manage your credit transactions",
"tabs": {
"balance": "Balance",
"transactions": "Transactions"
},
"balance": {
"title": "Credit Balance",
"description": "Your credit balance",
"credits": "Credits",
"creditsDescription": "You have {credits} credits",
"creditsExpired": "Credits expired",
"creditsAdded": "Credits have been added to your account",
"viewTransactions": "View Credit Transactions",
"retry": "Retry",
"expiringCredits": "{credits} credits expiring in the next {days} days"
},
"packages": {
"title": "Credit Packages",
"description": "Purchase additional credits to use our services",
"purchase": "Purchase",
"processing": "Processing...",
"popular": "Popular",
"completePurchase": "Complete Your Purchase",
"failedToFetchCredits": "Failed to fetch credits",
"failedToCreatePaymentIntent": "Failed to create payment intent",
"failedToInitiatePayment": "Failed to initiate payment",
"cancel": "Cancel",
"purchaseFailed": "Purchase credits failed",
"checkoutFailed": "Failed to create checkout session",
"loading": "Loading...",
"pay": "Pay"
},
"transactions": {
"title": "Credit Transactions",
"error": "Failed to get credit transactions",
"search": "Search credit transactions...",
"paymentIdCopied": "Payment ID copied to clipboard",
"columns": {
"columns": "Columns",
"id": "ID",
"type": "Type",
"description": "Description",
"amount": "Amount",
"remainingAmount": "Remaining Amount",
"paymentId": "Payment ID",
"expirationDate": "Expiration Date",
"expirationDateProcessedAt": "Expiration Date Processed At",
"createdAt": "Created At",
"updatedAt": "Updated At"
},
"types": {
"MONTHLY_REFRESH": "Monthly Refresh",
"REGISTER_GIFT": "Register Gift",
"PURCHASE": "Purchased Credits",
"USAGE": "Consumed Credits",
"EXPIRE": "Expired Credits",
"SUBSCRIPTION_RENEWAL": "Subscription Renewal",
"LIFETIME_MONTHLY": "Lifetime Monthly"
},
"detailViewer": {
"title": "Credit Transaction Detail",
"close": "Close"
},
"expired": "Expired",
"never": "Never"
}
},
"notification": {
"title": "Notification",
@ -890,23 +1007,68 @@
}
},
"AITextPage": {
"title": "AI Text",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
"content": "Working in progress"
"title": "AI Text Demo",
"description": "Analyze web content with AI to extract key information, features, and insights",
"content": "Web Content Analyzer",
"subtitle": "Enter a website URL to get AI-powered analysis of its content",
"analyzer": {
"title": "Web Content Analyzer",
"description": "Analyze any website content using AI to extract structured information",
"placeholder": "Enter website URL (e.g., https://example.com)",
"button": "Analyze Website",
"loading": {
"scraping": "Scraping website content...",
"analyzing": "Analyzing content with AI..."
},
"results": {
"title": "Analysis Results",
"newAnalysis": "Analyze Another Website",
"sections": {
"title": "Title",
"description": "Description",
"introduction": "Introduction",
"features": "Features",
"pricing": "Pricing",
"useCases": "Use Cases",
"screenshot": "Website Screenshot"
}
},
"errors": {
"invalidUrl": "Please enter a valid URL starting with http:// or https://",
"analysisError": "Failed to analyze website. Please try again.",
"networkError": "Network error. Please check your connection and try again.",
"insufficientCredits": "Insufficient credits. Please purchase more credits to continue."
}
},
"features": {
"scraping": {
"title": "Smart Web Scraping",
"description": "Advanced web scraping technology extracts clean, structured content from any website"
},
"analysis": {
"title": "AI-Powered Analysis",
"description": "Intelligent AI analysis extracts key insights, features, and structured information"
},
"results": {
"title": "Structured Results",
"description": "Get organized, easy-to-read results with clear sections and actionable insights"
}
}
},
"AIImagePage": {
"title": "AI Image",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
"content": "Working in progress"
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
},
"AIChatPage": {
"title": "AI Chat",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
},
"AIVideoPage": {
"title": "AI Video",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
"content": "Working in progress"
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
},
"AIAudioPage": {
"title": "AI Audio",
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly",
"content": "Working in progress"
"description": "MkSaaS lets you make AI SaaS in days, simply and effortlessly"
}
}

View File

@ -5,6 +5,7 @@
"description": "MkSaaS 是构建 AI SaaS 的最佳模板,使用 MkSaaS 可以在几天内轻松构建您的 AI SaaS简单且毫不费力。"
},
"Common": {
"premium": "付费文章",
"login": "登录",
"logout": "退出",
"signUp": "注册",
@ -28,7 +29,21 @@
"saving": "保存中...",
"loading": "加载中...",
"cancel": "取消",
"logoutFailed": "退出失败"
"logoutFailed": "退出失败",
"table": {
"totalRecords": "总共 {count} 条记录",
"noResults": "无结果",
"loading": "加载中...",
"columns": "列",
"rowsPerPage": "每页行数",
"page": "页",
"firstPage": "第一页",
"lastPage": "最后一页",
"nextPage": "下一页",
"previousPage": "上一页",
"ascending": "升序",
"descending": "降序"
}
},
"PricingPage": {
"title": "价格",
@ -94,12 +109,29 @@
"feature-3": "专属支持",
"feature-4": "企业级安全",
"feature-5": "高级集成",
"feature-6": "自定义域名",
"feature-7": "自定义品牌",
"feature-8": "终身更新"
"feature-6": "自定义品牌",
"feature-7": "终身更新"
}
}
},
"CreditPackages": {
"basic": {
"name": "基础版",
"description": "基础版功能介绍放这里"
},
"standard": {
"name": "标准版",
"description": "标准版功能介绍放这里"
},
"premium": {
"name": "高级版",
"description": "高级版功能介绍放这里"
},
"enterprise": {
"name": "企业版",
"description": "企业版功能介绍放这里"
}
},
"NotFoundPage": {
"title": "404",
"message": "抱歉,您正在寻找的页面不存在",
@ -188,7 +220,9 @@
"hidePassword": "隐藏密码",
"or": "或以社媒账号登录",
"emailRequired": "请输入邮箱",
"passwordRequired": "请输入密码"
"passwordRequired": "请输入密码",
"captchaInvalid": "验证码验证失败",
"captchaError": "验证码验证出错"
},
"register": {
"title": "注册",
@ -203,7 +237,9 @@
"hidePassword": "隐藏密码",
"nameRequired": "请输入姓名",
"emailRequired": "请输入邮箱",
"passwordRequired": "请输入密码"
"passwordRequired": "请输入密码",
"captchaInvalid": "验证码验证失败",
"captchaError": "验证码验证出错"
},
"forgotPassword": {
"title": "忘记密码",
@ -257,8 +293,20 @@
"nextPage": "下一页",
"chooseLanguage": "选择语言",
"title": "MkSaaS文档",
"homepage": "首页",
"blog": "博客"
"homepage": "首页"
},
"PremiumContent": {
"title": "解锁付费内容",
"description": "订阅我们的付费计划,访问所有付费内容和独家内容。",
"upgradeCta": "立即升级",
"benefit1": "所有内容",
"benefit2": "独家内容",
"benefit3": "随时取消",
"signIn": "登录",
"loginRequired": "登录以继续阅读",
"loginDescription": "这是一篇付费内容,请登录您的账户以访问完整内容。",
"checkingAccess": "检查阅读权限...",
"loadingContent": "加载完整内容..."
},
"Marketing": {
"navbar": {
@ -285,6 +333,10 @@
"title": "AI 图像",
"description": "展示如何使用 AI 生成精美图像"
},
"chat": {
"title": "AI 聊天",
"description": "展示如何使用 AI 与客户聊天"
},
"video": {
"title": "AI 视频",
"description": "展示如何使用 AI 生成惊人视频"
@ -374,13 +426,13 @@
"comparator": {
"title": "Comparator 组件"
},
"faqs": {
"title": "FAQs 组件"
"faq": {
"title": "FAQ 组件"
},
"login": {
"title": "Login 组件"
},
"sign-up": {
"signup": {
"title": "Signup 组件"
},
"forgot-password": {
@ -431,6 +483,7 @@
"avatar": {
"dashboard": "工作台",
"billing": "账单",
"credits": "积分",
"settings": "设置"
}
},
@ -456,14 +509,6 @@
"banReason": "封禁原因",
"banExpires": "封禁到期时间"
},
"noResults": "没有结果",
"firstPage": "第一页",
"lastPage": "最后一页",
"nextPage": "下一页",
"previousPage": "上一页",
"rowsPerPage": "每页行数",
"page": "页",
"loading": "加载中...",
"admin": "管理员",
"user": "用户",
"email": {
@ -545,7 +590,8 @@
"createCustomerPortalFailed": "打开Stripe客户界面失败"
},
"price": "价格:",
"nextBillingDate": "下次账单日期:",
"periodStartDate": "周期开始日期:",
"periodEndDate": "周期结束日期:",
"trialEnds": "试用结束日期:",
"freePlanMessage": "您当前使用的是功能有限的免费方案",
"lifetimeMessage": "您拥有所有高级功能的终身使用权限",
@ -553,7 +599,77 @@
"manageBilling": "管理账单",
"upgradePlan": "升级方案",
"retry": "重试",
"errorMessage": "获取数据失败"
"errorMessage": "获取数据失败",
"paymentSuccess": "支付成功"
},
"credits": {
"title": "积分",
"description": "管理您的积分交易",
"tabs": {
"balance": "积分余额",
"transactions": "交易记录"
},
"balance": {
"title": "积分余额",
"description": "您的积分余额",
"credits": "积分",
"creditsDescription": "您有 {credits} 积分",
"creditsExpired": "积分已过期",
"creditsAdded": "积分已添加到您的账户",
"viewTransactions": "查看积分记录",
"retry": "重试",
"expiringCredits": "{credits} 积分将在 {days} 天内过期"
},
"packages": {
"title": "积分套餐",
"description": "购买积分以使用我们的更多服务",
"purchase": "购买",
"processing": "处理中...",
"popular": "热门",
"completePurchase": "请支付订单",
"failedToFetchCredits": "获取积分失败",
"failedToCreatePaymentIntent": "创建付款意向失败",
"failedToInitiatePayment": "发起付款失败",
"cancel": "取消",
"purchaseFailed": "购买积分失败",
"checkoutFailed": "创建支付会话失败",
"loading": "加载中...",
"pay": "支付"
},
"transactions": {
"title": "积分记录",
"error": "获取积分交易记录失败",
"search": "搜索积分交易记录...",
"paymentIdCopied": "支付ID已复制到剪贴板",
"columns": {
"columns": "列",
"id": "ID",
"type": "类型",
"description": "描述",
"amount": "金额",
"remainingAmount": "剩余金额",
"paymentId": "支付编号",
"expirationDate": "过期日期",
"expirationDateProcessedAt": "过期处理时间",
"createdAt": "创建时间",
"updatedAt": "更新时间"
},
"types": {
"MONTHLY_REFRESH": "每月赠送",
"REGISTER_GIFT": "注册赠送",
"PURCHASE": "购买积分",
"USAGE": "使用积分",
"EXPIRE": "过期积分",
"SUBSCRIPTION_RENEWAL": "订阅月度积分",
"LIFETIME_MONTHLY": "终身月度积分"
},
"detailViewer": {
"title": "积分交易详情",
"close": "关闭"
},
"expired": "已过期",
"never": "永不过期"
}
},
"notification": {
"title": "通知",
@ -892,22 +1008,67 @@
},
"AITextPage": {
"title": "AI 文本",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力",
"content": "正在开发中"
"description": "使用 AI 分析网页内容,提取关键信息、功能和见解",
"content": "网页内容分析器",
"subtitle": "输入网站 URL使用 AI 分析其内容",
"analyzer": {
"title": "网页内容分析器",
"description": "使用 AI 分析任何网站的内容,提取结构化信息",
"placeholder": "输入网站 URL例如https://example.com",
"button": "分析网站",
"loading": {
"scraping": "正在抓取网站内容...",
"analyzing": "正在使用 AI 分析内容..."
},
"results": {
"title": "分析结果",
"newAnalysis": "分析其他网站",
"sections": {
"title": "标题",
"description": "描述",
"introduction": "介绍",
"features": "功能",
"pricing": "定价",
"useCases": "使用场景",
"screenshot": "网站截图"
}
},
"errors": {
"invalidUrl": "请输入以 http:// 或 https:// 开头的有效 URL",
"analysisError": "分析网站失败,请重试。",
"networkError": "网络错误,请检查您的连接并重试。",
"insufficientCredits": "积分不足,请购买更多积分以继续。"
}
},
"features": {
"scraping": {
"title": "智能网页抓取",
"description": "先进的网页抓取技术从任何网站提取干净、结构化的内容"
},
"analysis": {
"title": "AI 驱动分析",
"description": "智能 AI 分析提取关键见解、功能和结构化信息"
},
"results": {
"title": "结构化结果",
"description": "获得有组织、易于阅读的结果,包含清晰的部分和可操作的见解"
}
}
},
"AIImagePage": {
"title": "AI 图片",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力",
"content": "正在开发中"
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"
},
"AIChatPage": {
"title": "AI 聊天",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"
},
"AIVideoPage": {
"title": "AI 视频",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力",
"content": "正在开发中"
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"
},
"AIAudioPage": {
"title": "AI 音频",
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力",
"content": "正在开发中"
"description": "MkSaaS 让您在几天内轻松构建您的 AI SaaS简单且毫不费力"
}
}

View File

@ -1,4 +1,4 @@
import { withContentCollections } from '@content-collections/next';
import { createMDX } from 'fumadocs-mdx/next';
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
@ -6,13 +6,28 @@ import createNextIntlPlugin from 'next-intl/plugin';
* https://nextjs.org/docs/app/api-reference/config/next-config-js
*/
const nextConfig: NextConfig = {
// Docker standalone output
...(process.env.DOCKER_BUILD === 'true' && { output: 'standalone' }),
/* config options here */
devIndicators: false,
// https://nextjs.org/docs/architecture/nextjs-compiler#remove-console
// Remove all console.* calls in production only
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
// removeConsole: process.env.NODE_ENV === 'production',
},
// https://github.com/vercel/next.js/discussions/50177#discussioncomment-6006702
// fix build error: Module build failed: UnhandledSchemeError:
// Reading from "cloudflare:sockets" is not handled by plugins (Unhandled scheme).
webpack: (config, { webpack }) => {
config.plugins.push(
new webpack.IgnorePlugin({
resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
})
);
return config;
},
images: {
@ -41,6 +56,14 @@ const nextConfig: NextConfig = {
protocol: 'https',
hostname: 'ik.imagekit.io',
},
{
protocol: 'https',
hostname: 'html.tailus.io',
},
{
protocol: 'https',
hostname: 'service.firecrawl.dev',
},
],
},
};
@ -53,8 +76,15 @@ const nextConfig: NextConfig = {
const withNextIntl = createNextIntlPlugin();
/**
* withContentCollections must be the outermost plugin
*
* https://www.content-collections.dev/docs/quickstart/next
* https://fumadocs.dev/docs/ui/manual-installation
* https://fumadocs.dev/docs/mdx/plugin
*/
export default withContentCollections(withNextIntl(nextConfig));
const withMDX = createMDX();
export default withMDX(withNextIntl(nextConfig));
// https://opennext.js.org/cloudflare/get-started#12-develop-locally
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
// during local development, to access in any of your server code, local versions of Cloudflare bindings
initOpenNextCloudflareForDev();

6
open-next.config.ts Normal file
View File

@ -0,0 +1,6 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
});

View File

@ -3,9 +3,11 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"content-collections watch\" \"next dev\"",
"build": "content-collections build && next build",
"dev": "next dev",
"cf-dev": "next dev -p 8787",
"build": "next build",
"start": "next start",
"postinstall": "fumadocs-mdx",
"lint": "biome check --write .",
"lint:fix": "biome check --fix --unsafe .",
"format": "biome format --write .",
@ -14,25 +16,35 @@
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"list-contacts": "tsx scripts/list-contacts.ts",
"docs": "content-collections build",
"email": "email dev --dir src/mail/templates --port 3333"
"list-users": "tsx scripts/list-users.ts",
"content": "fumadocs-mdx",
"email": "email dev --dir src/mail/templates --port 3333",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
"knip": "knip"
},
"dependencies": {
"@ai-sdk/openai": "^1.1.13",
"@aws-sdk/client-s3": "^3.758.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@ai-sdk/deepseek": "^1.0.0",
"@ai-sdk/fal": "^1.0.0",
"@ai-sdk/fireworks": "^1.0.0",
"@ai-sdk/google": "^2.0.0",
"@ai-sdk/openai": "^2.0.0",
"@ai-sdk/react": "^2.0.22",
"@ai-sdk/replicate": "^1.0.0",
"@base-ui-components/react": "1.0.0-beta.0",
"@better-fetch/fetch": "^1.1.18",
"@content-collections/core": "^0.8.0",
"@content-collections/mdx": "^0.2.0",
"@content-collections/next": "^0.2.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fumadocs/content-collections": "^1.1.8",
"@hookform/resolvers": "^4.1.0",
"@hookform/resolvers": "^5.2.1",
"@marsidev/react-turnstile": "^1.1.0",
"@mendable/firecrawl-js": "^1.29.1",
"@next/third-parties": "^15.3.0",
"@openpanel/nextjs": "^1.0.7",
"@openrouter/ai-sdk-provider": "^1.0.0-beta.6",
"@orama/orama": "^3.1.4",
"@orama/tokenizers": "^3.1.4",
"@radix-ui/react-accordion": "^1.2.3",
@ -64,22 +76,25 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@react-email/components": "0.0.33",
"@react-email/render": "1.0.5",
"@stripe/stripe-js": "^5.6.0",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-table": "^8.21.2",
"@types/canvas-confetti": "^1.9.0",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"@widgetbot/react-embed": "^1.9.0",
"ai": "^4.1.45",
"ai": "^5.0.0",
"better-auth": "^1.1.19",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.1.1",
"cookie": "^1.0.2",
"crisp-sdk-web": "^1.0.25",
"date-fns": "^4.1.0",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.7",
@ -87,63 +102,65 @@
"drizzle-orm": "^0.39.3",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^12.4.7",
"fumadocs-core": "^15.1.2",
"fumadocs-ui": "^15.1.2",
"fumadocs-core": "^15.6.7",
"fumadocs-mdx": "^11.7.3",
"fumadocs-ui": "^15.6.7",
"input-otp": "^1.4.2",
"lucide-react": "^0.483.0",
"mdast-util-toc": "^7.1.0",
"motion": "^12.4.3",
"next": "15.2.1",
"next-intl": "^4.0.0",
"next-plausible": "^3.12.4",
"next-safe-action": "^7.10.4",
"next-themes": "^0.4.4",
"pg": "^8.16.0",
"nuqs": "^2.5.1",
"postgres": "^3.4.5",
"radix-ui": "^1.4.2",
"react": "^19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-hook-form": "^7.62.0",
"react-remove-scroll": "^2.6.3",
"react-resizable-panels": "^2.1.7",
"react-syntax-highlighter": "^15.6.3",
"react-tweet": "^3.2.2",
"react-use-measure": "^2.1.7",
"recharts": "^2.15.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.0",
"rehype-slug": "^6.0.0",
"remark": "^15.0.1",
"remark-code-import": "^1.2.0",
"remark-gfm": "^4.0.1",
"resend": "^4.4.1",
"s3mini": "^0.2.0",
"shiki": "^2.4.2",
"sonner": "^2.0.0",
"streamdown": "^1.0.12",
"stripe": "^17.6.0",
"swiper": "^11.2.5",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.4",
"unist-util-visit": "^5.0.0",
"use-intl": "^3.26.5",
"use-media": "^1.5.0",
"use-stick-to-bottom": "^1.1.1",
"vaul": "^1.1.2",
"zod": "^3.24.2",
"zod": "^4.0.17",
"zustand": "^5.0.3"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@content-collections/cli": "^0.1.6",
"@opennextjs/cloudflare": "^1.6.5",
"@tailwindcss/postcss": "^4.0.14",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@types/mdx": "^2.0.13",
"@types/node": "^20",
"@types/node": "^20.19.0",
"@types/pg": "^8.11.11",
"@types/react": "^19",
"@types/react-dom": "^19",
"concurrently": "^9.1.2",
"@types/react-syntax-highlighter": "^15.5.13",
"drizzle-kit": "^0.30.4",
"knip": "^5.61.2",
"postcss": "^8",
"react-email": "3.0.7",
"tailwindcss": "^4.0.14",
"tsx": "^4.19.3",
"typescript": "^5"
"typescript": "^5.8.3",
"wrangler": "^4.28.1"
}
}

9046
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
public/_headers Normal file
View File

@ -0,0 +1,2 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable

View File

@ -1,129 +0,0 @@
// Service Worker for caching iframe content
const CACHE_NAME = 'cnblocks-iframe-cache-v1'
// Add iframe URLs to this list to prioritize caching
const URLS_TO_CACHE = [
// Default assets that should be cached
'/favicon.ico',
// Images used in iframes
'/payments.png',
'/payments-light.png',
'/origin-cal.png',
'/origin-cal-dark.png',
'/exercice.png',
'/exercice-dark.png',
'/charts-light.png',
'/charts.png',
'/music-light.png',
'/music.png',
'/mail-back-light.png',
'/mail-upper.png',
'/mail-back.png',
'/card.png',
'/dark-card.webp',
]
// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache')
return cache.addAll(URLS_TO_CACHE)
})
.then(() => self.skipWaiting()) // Activate SW immediately
)
})
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
const currentCaches = [CACHE_NAME]
event.waitUntil(
caches
.keys()
.then((cacheNames) => {
return cacheNames.filter((cacheName) => !currentCaches.includes(cacheName))
})
.then((cachesToDelete) => {
return Promise.all(
cachesToDelete.map((cacheToDelete) => {
return caches.delete(cacheToDelete)
})
)
})
.then(() => self.clients.claim()) // Take control of clients immediately
)
})
// Fetch event - serve from cache or fetch from network and cache
self.addEventListener('fetch', (event) => {
// Check if this is an iframe request - typically they'll be HTML or have 'preview' in the URL
const isIframeRequest = event.request.url.includes('/preview/') || event.request.url.includes('/examples/')
if (isIframeRequest) {
event.respondWith(
caches.match(event.request, { ignoreSearch: true }).then((response) => {
// Return cached response if found
if (response) {
return response
}
// Clone the request (requests are one-time use)
const fetchRequest = event.request.clone()
return fetch(fetchRequest).then((response) => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response
}
// Clone the response (responses are one-time use)
const responseToCache = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache)
})
return response
})
})
)
} else {
// For non-iframe requests, use a standard cache-first strategy
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response
}
return fetch(event.request)
})
)
}
})
// Listen for messages from clients (to force cache update, etc)
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
// Handle cache clearing
if (event.data && event.data.type === 'CLEAR_IFRAME_CACHE') {
const url = event.data.url
if (url) {
// Clear specific URL from cache
caches.open(CACHE_NAME).then((cache) => {
cache.delete(url).then(() => {
console.log(`Cleared cache for: ${url}`)
})
})
} else {
// Clear the entire cache
caches.delete(CACHE_NAME).then(() => {
console.log('Cleared entire iframe cache')
})
}
}
})

24
scripts/list-users.ts Normal file
View File

@ -0,0 +1,24 @@
import dotenv from 'dotenv';
import { getDb } from '../src/db/index.js';
import { user } from '../src/db/schema.js';
dotenv.config();
export default async function listUsers() {
const db = await getDb();
try {
const users = await db.select({ email: user.email }).from(user);
// Extract emails from users
const emails: string[] = users.map((user) => user.email);
console.log(`Total users: ${emails.length}`);
// Output all emails joined with comma
console.log(emails.join(', '));
} catch (error) {
console.error('Error fetching users:', error);
}
}
listUsers();

102
source.config.ts Normal file
View File

@ -0,0 +1,102 @@
import {
defineCollections,
defineDocs,
frontmatterSchema,
metaSchema,
} from 'fumadocs-mdx/config';
import { z } from 'zod';
/**
* https://fumadocs.dev/docs/mdx/collections#schema-1
*/
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: frontmatterSchema.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
premium: z.boolean().optional(),
}),
},
meta: {
schema: metaSchema.extend({
description: z.string().optional(),
}),
},
});
/**
* Changelog
*
* title is required, but description is optional in frontmatter
*/
export const changelog = defineCollections({
type: 'doc',
dir: 'content/changelog',
schema: frontmatterSchema.extend({
version: z.string(),
date: z.string().date(),
published: z.boolean().default(true),
}),
});
/**
* Pages, like privacy policy, terms of service, etc.
*
* title is required, but description is optional in frontmatter
*/
export const pages = defineCollections({
type: 'doc',
dir: 'content/pages',
schema: frontmatterSchema.extend({
date: z.string().date(),
published: z.boolean().default(true),
}),
});
/**
* Blog authors
*
* description is optional in frontmatter, but we must add it to the schema
*/
export const author = defineCollections({
type: 'doc',
dir: 'content/author',
schema: z.object({
name: z.string(),
avatar: z.string(),
description: z.string().optional(),
}),
});
/**
* Blog categories
*
* description is optional in frontmatter, but we must add it to the schema
*/
export const category = defineCollections({
type: 'doc',
dir: 'content/category',
schema: z.object({
name: z.string(),
description: z.string().optional(),
}),
});
/**
* Blog posts
*
* title is required, but description is optional in frontmatter
*/
export const blog = defineCollections({
type: 'doc',
dir: 'content/blog',
schema: frontmatterSchema.extend({
image: z.string(),
date: z.string().date(),
published: z.boolean().default(true),
premium: z.boolean().optional(),
categories: z.array(z.string()),
author: z.string(),
}),
});

View File

@ -1,19 +1,16 @@
'use server';
import { userActionClient } from '@/lib/safe-action';
import { isSubscribed } from '@/newsletter';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation
const newsletterSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email address' }),
email: z.email({ error: 'Please enter a valid email address' }),
});
// Create a safe action to check if a user is subscribed to the newsletter
export const checkNewsletterStatusAction = actionClient
export const checkNewsletterStatusAction = userActionClient
.schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => {
try {

View File

@ -0,0 +1,37 @@
'use server';
import { consumeCredits } from '@/credits/credits';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { z } from 'zod';
// consume credits schema
const consumeSchema = z.object({
amount: z.number().min(1),
description: z.string().optional(),
});
/**
* Consume credits
*/
export const consumeCreditsAction = userActionClient
.schema(consumeSchema)
.action(async ({ parsedInput, ctx }) => {
const { amount, description } = parsedInput;
const currentUser = (ctx as { user: User }).user;
try {
await consumeCredits({
userId: currentUser.id,
amount,
description: description || `Consume credits: ${amount}`,
});
return { success: true };
} catch (error) {
console.error('consume credits error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Something went wrong',
};
}
});

View File

@ -1,57 +1,34 @@
'use server';
import { websiteConfig } from '@/config/website';
import type { User } from '@/lib/auth-types';
import { findPlanByPlanId } from '@/lib/price-plan';
import { getSession } from '@/lib/server';
import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCheckout } from '@/payment';
import type { CreateCheckoutParams } from '@/payment/types';
import { Routes } from '@/routes';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { cookies } from 'next/headers';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Checkout schema for validation
// metadata is optional, and may contain referral information if you need
const checkoutSchema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
planId: z.string().min(1, { message: 'Plan ID is required' }),
priceId: z.string().min(1, { message: 'Price ID is required' }),
metadata: z.record(z.string()).optional(),
userId: z.string().min(1, { error: 'User ID is required' }),
planId: z.string().min(1, { error: 'Plan ID is required' }),
priceId: z.string().min(1, { error: 'Price ID is required' }),
metadata: z.record(z.string(), z.string()).optional(),
});
/**
* Create a checkout session for a price plan
*/
export const createCheckoutAction = actionClient
export const createCheckoutAction = userActionClient
.schema(checkoutSchema)
.action(async ({ parsedInput }) => {
const { userId, planId, priceId, metadata } = parsedInput;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to create their own checkout session
if (session.user.id !== userId) {
console.warn(
`current user ${session.user.id} is not authorized to create checkout session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
.action(async ({ parsedInput, ctx }) => {
const { planId, priceId, metadata } = parsedInput;
const currentUser = (ctx as { user: User }).user;
try {
// Get the current locale from the request
@ -62,27 +39,37 @@ export const createCheckoutAction = actionClient
if (!plan) {
return {
success: false,
error: 'Plan not found',
error: 'Price plan not found',
};
}
// Add user id to metadata, so we can get it in the webhook event
const customMetadata = {
const customMetadata: Record<string, string> = {
...metadata,
userId: session.user.id,
userName: session.user.name,
userId: currentUser.id,
userName: currentUser.name,
};
// https://datafa.st/docs/stripe-checkout-api
// if datafast analytics is enabled, add the revenue attribution to the metadata
if (websiteConfig.features.enableDatafastRevenueTrack) {
const cookieStore = await cookies();
customMetadata.datafast_visitor_id =
cookieStore.get('datafast_visitor_id')?.value ?? '';
customMetadata.datafast_session_id =
cookieStore.get('datafast_session_id')?.value ?? '';
}
// Create the checkout session with localized URLs
const successUrl = getUrlWithLocale(
'/settings/billing?session_id={CHECKOUT_SESSION_ID}',
`${Routes.SettingsBilling}?session_id={CHECKOUT_SESSION_ID}`,
locale
);
const cancelUrl = getUrlWithLocale(Routes.Pricing, locale);
const params: CreateCheckoutParams = {
planId,
priceId,
customerEmail: session.user.email,
customerEmail: currentUser.email,
metadata: customMetadata,
successUrl,
cancelUrl,

View File

@ -0,0 +1,99 @@
'use server';
import { websiteConfig } from '@/config/website';
import { getCreditPackageById } from '@/credits/server';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCreditCheckout } from '@/payment';
import type { CreateCreditCheckoutParams } from '@/payment/types';
import { Routes } from '@/routes';
import { getLocale } from 'next-intl/server';
import { cookies } from 'next/headers';
import { z } from 'zod';
// Credit checkout schema for validation
// metadata is optional, and may contain referral information if you need
const creditCheckoutSchema = z.object({
userId: z.string().min(1, { error: 'User ID is required' }),
packageId: z.string().min(1, { error: 'Package ID is required' }),
priceId: z.string().min(1, { error: 'Price ID is required' }),
metadata: z.record(z.string(), z.string()).optional(),
});
/**
* Create a checkout session for a credit package
*/
export const createCreditCheckoutSession = userActionClient
.schema(creditCheckoutSchema)
.action(async ({ parsedInput, ctx }) => {
const { packageId, priceId, metadata } = parsedInput;
const currentUser = (ctx as { user: User }).user;
try {
// Get the current locale from the request
const locale = await getLocale();
// Find the credit package
const creditPackage = getCreditPackageById(packageId);
if (!creditPackage) {
return {
success: false,
error: 'Credit package not found',
};
}
// Add metadata to identify this as a credit purchase
const customMetadata: Record<string, string> = {
...metadata,
type: 'credit_purchase',
packageId,
credits: creditPackage.amount.toString(),
userId: currentUser.id,
userName: currentUser.name,
};
// https://datafa.st/docs/stripe-checkout-api
// if datafast analytics is enabled, add the revenue attribution to the metadata
if (websiteConfig.features.enableDatafastRevenueTrack) {
const cookieStore = await cookies();
customMetadata.datafast_visitor_id =
cookieStore.get('datafast_visitor_id')?.value ?? '';
customMetadata.datafast_session_id =
cookieStore.get('datafast_session_id')?.value ?? '';
}
// Create checkout session with credit-specific URLs
const successUrl = getUrlWithLocale(
`${Routes.SettingsCredits}?credits_session_id={CHECKOUT_SESSION_ID}`,
locale
);
const cancelUrl = getUrlWithLocale(Routes.SettingsCredits, locale);
const params: CreateCreditCheckoutParams = {
packageId,
priceId,
customerEmail: currentUser.email,
metadata: customMetadata,
successUrl,
cancelUrl,
locale,
};
const result = await createCreditCheckout(params);
// console.log('create credit checkout session result:', result);
return {
success: true,
data: result,
};
} catch (error) {
console.error('Create credit checkout session error:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Failed to create checkout session',
};
}
});

View File

@ -1,69 +1,45 @@
'use server';
import db from '@/db';
import { getDb } from '@/db';
import { user } from '@/db/schema';
import { getSession } from '@/lib/server';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getUrlWithLocale } from '@/lib/urls/urls';
import { createCustomerPortal } from '@/payment';
import type { CreatePortalParams } from '@/payment/types';
import { eq } from 'drizzle-orm';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Portal schema for validation
const portalSchema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
userId: z.string().min(1, { error: 'User ID is required' }),
returnUrl: z
.string()
.url({ message: 'Return URL must be a valid URL' })
.url({ error: 'Return URL must be a valid URL' })
.optional(),
});
/**
* Create a customer portal session
*/
export const createPortalAction = actionClient
export const createPortalAction = userActionClient
.schema(portalSchema)
.action(async ({ parsedInput }) => {
const { userId, returnUrl } = parsedInput;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to create portal session for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to create their own portal session
if (session.user.id !== userId) {
console.warn(
`current user ${session.user.id} is not authorized to create portal session for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
.action(async ({ parsedInput, ctx }) => {
const { returnUrl } = parsedInput;
const currentUser = (ctx as { user: User }).user;
try {
// Get the user's customer ID from the database
const db = await getDb();
const customerResult = await db
.select({ customerId: user.customerId })
.from(user)
.where(eq(user.id, session.user.id))
.where(eq(user.id, currentUser.id))
.limit(1);
if (customerResult.length <= 0 || !customerResult[0].customerId) {
console.error(`No customer found for user ${session.user.id}`);
console.error(`No customer found for user ${currentUser.id}`);
return {
success: false,
error: 'No customer found for user',

View File

@ -1,16 +1,13 @@
'use server';
import { getSession } from '@/lib/server';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { getSubscriptions } from '@/payment';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Input schema
const schema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
userId: z.string().min(1, { error: 'User ID is required' }),
});
/**
@ -19,38 +16,27 @@ const schema = z.object({
* If the user has multiple subscriptions,
* it returns the most recent active or trialing one
*/
export const getActiveSubscriptionAction = actionClient
export const getActiveSubscriptionAction = userActionClient
.schema(schema)
.action(async ({ parsedInput }) => {
const { userId } = parsedInput;
.action(async ({ ctx }) => {
const currentUser = (ctx as { user: User }).user;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to get active subscription for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Check if Stripe environment variables are configured
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(
`current user ${session.user.id} is not authorized to get active subscription for user ${userId}`
);
if (!stripeSecretKey || !stripeWebhookSecret) {
console.log('Stripe environment variables not configured, return');
return {
success: false,
error: 'Not authorized to do this action',
success: true,
data: null, // No subscription = free plan
};
}
try {
// Find the user's most recent active subscription
const subscriptions = await getSubscriptions({
userId: session.user.id,
userId: currentUser.id,
});
// console.log('get user subscriptions:', subscriptions);
@ -64,16 +50,16 @@ export const getActiveSubscriptionAction = actionClient
// If found, use it
if (activeSubscription) {
console.log('find active subscription for userId:', session.user.id);
console.log('find active subscription for userId:', currentUser.id);
subscriptionData = activeSubscription;
} else {
console.log(
'no active subscription found for userId:',
session.user.id
currentUser.id
);
}
} else {
console.log('no subscriptions found for userId:', session.user.id);
console.log('no subscriptions found for userId:', currentUser.id);
}
return {

View File

@ -0,0 +1,27 @@
'use server';
import { getUserCredits } from '@/credits/credits';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
/**
* Get current user's credits
*/
export const getCreditBalanceAction = userActionClient.action(
async ({ ctx }) => {
try {
const currentUser = (ctx as { user: User }).user;
const credits = await getUserCredits(currentUser.id);
return { success: true, credits };
} catch (error) {
console.error('get credit balance error:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Failed to fetch credit balance',
};
}
}
);

View File

@ -0,0 +1,62 @@
'use server';
import { getDb } from '@/db';
import { creditTransaction } from '@/db/schema';
import type { User } from '@/lib/auth-types';
import { CREDITS_EXPIRATION_DAYS } from '@/lib/constants';
import { userActionClient } from '@/lib/safe-action';
import { addDays } from 'date-fns';
import { and, eq, gt, gte, isNotNull, lte, sum } from 'drizzle-orm';
/**
* Get credit statistics for a user
*/
export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => {
try {
const currentUser = (ctx as { user: User }).user;
const userId = currentUser.id;
const db = await getDb();
const now = new Date();
// Get credits expiring in the next 30 days
const expirationDaysFromNow = addDays(now, CREDITS_EXPIRATION_DAYS);
// Get total credits expiring in the next 30 days
const expiringCreditsResult = await db
.select({
totalAmount: sum(creditTransaction.remainingAmount),
})
.from(creditTransaction)
.where(
and(
eq(creditTransaction.userId, userId),
isNotNull(creditTransaction.expirationDate),
isNotNull(creditTransaction.remainingAmount),
gt(creditTransaction.remainingAmount, 0),
lte(creditTransaction.expirationDate, expirationDaysFromNow),
gte(creditTransaction.expirationDate, now)
)
);
const totalExpiringCredits =
Number(expiringCreditsResult[0]?.totalAmount) || 0;
return {
success: true,
data: {
expiringCredits: {
amount: totalExpiringCredits,
},
},
};
} catch (error) {
console.error('get credit stats error:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Failed to fetch credit statistics',
};
}
});

View File

@ -0,0 +1,128 @@
'use server';
import { getDb } from '@/db';
import { creditTransaction } from '@/db/schema';
import type { User } from '@/lib/auth-types';
import { userActionClient } from '@/lib/safe-action';
import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm';
import { z } from 'zod';
// Define the schema for getCreditTransactions parameters
const getCreditTransactionsSchema = z.object({
pageIndex: z.number().min(0).default(0),
pageSize: z.number().min(1).max(100).default(10),
search: z.string().optional().default(''),
sorting: z
.array(
z.object({
id: z.string(),
desc: z.boolean(),
})
)
.optional()
.default([]),
});
// Define sort field mapping
const sortFieldMap = {
type: creditTransaction.type,
amount: creditTransaction.amount,
remainingAmount: creditTransaction.remainingAmount,
description: creditTransaction.description,
createdAt: creditTransaction.createdAt,
updatedAt: creditTransaction.updatedAt,
expirationDate: creditTransaction.expirationDate,
expirationDateProcessedAt: creditTransaction.expirationDateProcessedAt,
paymentId: creditTransaction.paymentId,
} as const;
// Create a safe action for getting credit transactions
export const getCreditTransactionsAction = userActionClient
.schema(getCreditTransactionsSchema)
.action(async ({ parsedInput, ctx }) => {
try {
const { pageIndex, pageSize, search, sorting } = parsedInput;
const currentUser = (ctx as { user: User }).user;
// Search logic: text fields use ilike, and if search is a number, also search amount fields
const searchConditions = [];
if (search) {
// Always search text fields
searchConditions.push(
ilike(creditTransaction.type, `%${search}%`),
ilike(creditTransaction.paymentId, `%${search}%`),
ilike(creditTransaction.description, `%${search}%`)
);
// If search is a valid number, also search numeric fields
const numericSearch = Number.parseInt(search, 10);
if (!Number.isNaN(numericSearch)) {
searchConditions.push(
eq(creditTransaction.amount, numericSearch),
eq(creditTransaction.remainingAmount, numericSearch)
);
}
}
const where = search
? and(
eq(creditTransaction.userId, currentUser.id),
or(...searchConditions)
)
: eq(creditTransaction.userId, currentUser.id);
const offset = pageIndex * pageSize;
// Get the sort configuration
const sortConfig = sorting[0];
const sortField = sortConfig?.id
? sortFieldMap[sortConfig.id as keyof typeof sortFieldMap]
: creditTransaction.createdAt;
const sortDirection = sortConfig?.desc ? desc : asc;
const db = await getDb();
const [items, [{ count }]] = await Promise.all([
db
.select({
id: creditTransaction.id,
userId: creditTransaction.userId,
type: creditTransaction.type,
description: creditTransaction.description,
amount: creditTransaction.amount,
remainingAmount: creditTransaction.remainingAmount,
paymentId: creditTransaction.paymentId,
expirationDate: creditTransaction.expirationDate,
expirationDateProcessedAt:
creditTransaction.expirationDateProcessedAt,
createdAt: creditTransaction.createdAt,
updatedAt: creditTransaction.updatedAt,
})
.from(creditTransaction)
.where(where)
.orderBy(sortDirection(sortField))
.limit(pageSize)
.offset(offset),
db
.select({ count: sql`count(*)` })
.from(creditTransaction)
.where(where),
]);
return {
success: true,
data: {
items,
total: Number(count),
},
};
} catch (error) {
console.error('get credit transactions error:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Failed to fetch credit transactions',
};
}
});

View File

@ -1,20 +1,17 @@
'use server';
import db from '@/db';
import { getDb } from '@/db';
import { payment } from '@/db/schema';
import type { User } from '@/lib/auth-types';
import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan';
import { getSession } from '@/lib/server';
import { userActionClient } from '@/lib/safe-action';
import { PaymentTypes } from '@/payment/types';
import { and, eq } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Input schema
const schema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
userId: z.string().min(1, { error: 'User ID is required' }),
});
/**
@ -25,33 +22,11 @@ const schema = z.object({
* in order to do this, you have to update the logic to check the lifetime status,
* for example, just check the planId is `lifetime` or not.
*/
export const getLifetimeStatusAction = actionClient
export const getLifetimeStatusAction = userActionClient
.schema(schema)
.action(async ({ parsedInput }) => {
const { userId } = parsedInput;
// Get the current user session for authorization
const session = await getSession();
if (!session) {
console.warn(
`unauthorized request to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Unauthorized',
};
}
// Only allow users to check their own status unless they're admins
if (session.user.id !== userId && session.user.role !== 'admin') {
console.warn(
`current user ${session.user.id} is not authorized to get lifetime status for user ${userId}`
);
return {
success: false,
error: 'Not authorized to do this action',
};
}
.action(async ({ ctx }) => {
const currentUser = (ctx as { user: User }).user;
const userId = currentUser.id;
try {
// Get lifetime plans
@ -69,6 +44,7 @@ export const getLifetimeStatusAction = actionClient
}
// Query the database for one-time payments with lifetime plans
const db = await getDb();
const result = await db
.select({
id: payment.id,

View File

@ -1,14 +1,12 @@
'use server';
import db from '@/db';
import { getDb } from '@/db';
import { user } from '@/db/schema';
import { isDemoWebsite } from '@/lib/demo';
import { adminActionClient } from '@/lib/safe-action';
import { asc, desc, ilike, or, sql } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Define the schema for getUsers parameters
const getUsersSchema = z.object({
pageIndex: z.number().min(0).default(0),
@ -38,14 +36,19 @@ const sortFieldMap = {
} as const;
// Create a safe action for getting users
export const getUsersAction = actionClient
export const getUsersAction = adminActionClient
.schema(getUsersSchema)
.action(async ({ parsedInput }) => {
try {
const { pageIndex, pageSize, search, sorting } = parsedInput;
// search by name, email, and customerId
const where = search
? or(ilike(user.name, `%${search}%`), ilike(user.email, `%${search}%`))
? or(
ilike(user.name, `%${search}%`),
ilike(user.email, `%${search}%`),
ilike(user.customerId, `%${search}%`)
)
: undefined;
const offset = pageIndex * pageSize;
@ -57,6 +60,7 @@ export const getUsersAction = actionClient
: user.createdAt;
const sortDirection = sortConfig?.desc ? desc : asc;
const db = await getDb();
let [items, [{ count }]] = await Promise.all([
db
.select()
@ -69,7 +73,8 @@ export const getUsersAction = actionClient
]);
// hide user data in demo website
if (process.env.NEXT_PUBLIC_DEMO_WEBSITE === 'true') {
const isDemo = isDemoWebsite();
if (isDemo) {
items = items.map((item) => ({
...item,
name: 'Demo User',

View File

@ -1,14 +1,11 @@
'use server';
import { websiteConfig } from '@/config/website';
import { actionClient } from '@/lib/safe-action';
import { sendEmail } from '@/mail';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
/**
* DOC: When using Zod for validation, how can I localize error messages?
* https://next-intl.dev/docs/environments/actions-metadata-route-handlers#server-actions
@ -17,13 +14,13 @@ const actionClient = createSafeActionClient();
const contactFormSchema = z.object({
name: z
.string()
.min(3, { message: 'Name must be at least 3 characters' })
.max(30, { message: 'Name must not exceed 30 characters' }),
email: z.string().email({ message: 'Please enter a valid email address' }),
.min(3, { error: 'Name must be at least 3 characters' })
.max(30, { error: 'Name must not exceed 30 characters' }),
email: z.email({ error: 'Please enter a valid email address' }),
message: z
.string()
.min(10, { message: 'Message must be at least 10 characters' })
.max(500, { message: 'Message must not exceed 500 characters' }),
.min(10, { error: 'Message must be at least 10 characters' })
.max(500, { error: 'Message must not exceed 500 characters' }),
});
// Create a safe action for contact form submission

View File

@ -1,17 +1,14 @@
'use server';
import { actionClient } from '@/lib/safe-action';
import { sendEmail } from '@/mail';
import { subscribe } from '@/newsletter';
import { getLocale } from 'next-intl/server';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation
const newsletterSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email address' }),
email: z.email({ error: 'Please enter a valid email address' }),
});
// Create a safe action for newsletter subscription

View File

@ -1,30 +1,18 @@
'use server';
import { getSession } from '@/lib/server';
import { userActionClient } from '@/lib/safe-action';
import { unsubscribe } from '@/newsletter';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Newsletter schema for validation
const newsletterSchema = z.object({
email: z.string().email({ message: 'Please enter a valid email address' }),
email: z.email({ error: 'Please enter a valid email address' }),
});
// Create a safe action for newsletter unsubscription
export const unsubscribeNewsletterAction = actionClient
export const unsubscribeNewsletterAction = userActionClient
.schema(newsletterSchema)
.action(async ({ parsedInput: { email } }) => {
const session = await getSession();
if (!session) {
return {
success: false,
error: 'Unauthorized',
};
}
try {
const unsubscribed = await unsubscribe(email);

View File

@ -0,0 +1,33 @@
'use server';
import { validateTurnstileToken } from '@/lib/captcha';
import { actionClient } from '@/lib/safe-action';
import { z } from 'zod';
// Captcha validation schema
const captchaSchema = z.object({
captchaToken: z.string().min(1, { error: 'Captcha token is required' }),
});
// Create a safe action for captcha validation
export const validateCaptchaAction = actionClient
.schema(captchaSchema)
.action(async ({ parsedInput }) => {
const { captchaToken } = parsedInput;
try {
const isValid = await validateTurnstileToken(captchaToken);
return {
success: true,
valid: isValid,
};
} catch (error) {
console.error('Captcha validation error:', error);
return {
success: false,
valid: false,
error: error instanceof Error ? error.message : 'Something went wrong',
};
}
});

View File

@ -0,0 +1,181 @@
'use client';
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import { Loader } from '@/components/ai-elements/loader';
import { Message, MessageContent } from '@/components/ai-elements/message';
import {
PromptInput,
PromptInputButton,
PromptInputModelSelect,
PromptInputModelSelectContent,
PromptInputModelSelectItem,
PromptInputModelSelectTrigger,
PromptInputModelSelectValue,
PromptInputSubmit,
PromptInputTextarea,
PromptInputToolbar,
PromptInputTools,
} from '@/components/ai-elements/prompt-input';
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from '@/components/ai-elements/reasoning';
import { Response } from '@/components/ai-elements/response';
import {
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from '@/components/ai-elements/source';
import { useChat } from '@ai-sdk/react';
import { GlobeIcon } from 'lucide-react';
import { useState } from 'react';
const models = [
{
name: 'GPT 4o',
value: 'openai/gpt-4o',
},
{
name: 'Deepseek R1',
value: 'deepseek/deepseek-r1',
},
];
export default function ChatBot() {
const [input, setInput] = useState('');
const [model, setModel] = useState<string>(models[0].value);
const [webSearch, setWebSearch] = useState(false);
const { messages, sendMessage, status } = useChat();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
sendMessage(
{ text: input },
{
body: {
model: model,
webSearch: webSearch,
},
}
);
setInput('');
}
};
return (
<div className="mx-auto p-6 relative size-full h-screen rounded-lg bg-muted">
<div className="flex flex-col h-full">
<Conversation className="h-full">
<ConversationContent>
{messages.map((message) => (
<div key={message.id}>
{message.role === 'assistant' && (
<Sources>
{message.parts.map((part, i) => {
switch (part.type) {
case 'source-url':
return (
<>
<SourcesTrigger
count={
message.parts.filter(
(part) => part.type === 'source-url'
).length
}
/>
<SourcesContent key={`${message.id}-${i}`}>
<Source
key={`${message.id}-${i}`}
href={part.url}
title={part.url}
/>
</SourcesContent>
</>
);
}
})}
</Sources>
)}
<Message from={message.role} key={message.id}>
<MessageContent>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return (
<Response key={`${message.id}-${i}`}>
{part.text}
</Response>
);
case 'reasoning':
return (
<Reasoning
key={`${message.id}-${i}`}
className="w-full"
isStreaming={status === 'streaming'}
>
<ReasoningTrigger />
<ReasoningContent>{part.text}</ReasoningContent>
</Reasoning>
);
default:
return null;
}
})}
</MessageContent>
</Message>
</div>
))}
{status === 'submitted' && <Loader />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={handleSubmit} className="mt-4">
<PromptInputTextarea
onChange={(e) => setInput(e.target.value)}
value={input}
/>
<PromptInputToolbar>
<PromptInputTools>
<PromptInputButton
variant={webSearch ? 'default' : 'ghost'}
onClick={() => setWebSearch(!webSearch)}
>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<PromptInputModelSelect
onValueChange={(value) => {
setModel(value);
}}
value={model}
>
<PromptInputModelSelectTrigger>
<PromptInputModelSelectValue />
</PromptInputModelSelectTrigger>
<PromptInputModelSelectContent>
{models.map((model) => (
<PromptInputModelSelectItem
key={model.value}
value={model.value}
>
{model.name}
</PromptInputModelSelectItem>
))}
</PromptInputModelSelectContent>
</PromptInputModelSelect>
</PromptInputTools>
<PromptInputSubmit disabled={!input} status={status} />
</PromptInputToolbar>
</PromptInput>
</div>
</div>
);
}

View File

@ -0,0 +1,102 @@
'use client';
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '@/components/ui/carousel';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react';
import type { GeneratedImage, ProviderTiming } from '../lib/image-types';
import type { ProviderKey } from '../lib/provider-config';
import { ImageDisplay } from './ImageDisplay';
interface ImageCarouselProps {
providers: ProviderKey[];
images: GeneratedImage[];
timings: Record<ProviderKey, ProviderTiming>;
failedProviders: ProviderKey[];
enabledProviders: Record<ProviderKey, boolean>;
providerToModel: Record<ProviderKey, string>;
}
export function ImageCarousel({
providers,
images,
timings,
failedProviders,
enabledProviders,
providerToModel,
}: ImageCarouselProps) {
const [currentSlide, setCurrentSlide] = useState(0);
const [api, setApi] = useState<CarouselApi>();
useEffect(() => {
if (!api) return;
api.on('select', () => {
setCurrentSlide(api.selectedScrollSnap());
});
}, [api]);
return (
<div className="relative w-full">
<Carousel setApi={setApi} opts={{ align: 'start', loop: true }}>
<CarouselContent>
{providers.map((provider, i) => {
const imageData = images?.find(
(img) => img.provider === provider
)?.image;
const timing = timings[provider];
return (
<CarouselItem key={provider}>
<ImageDisplay
modelId={
images?.find((img) => img.provider === provider)?.modelId ||
providerToModel[provider]
}
provider={provider}
image={imageData}
timing={timing}
failed={failedProviders.includes(provider)}
enabled={enabledProviders[provider]}
/>
<div className="text-center text-sm text-muted-foreground mt-4">
{i + 1} of {providers.length}
</div>
</CarouselItem>
);
})}
</CarouselContent>
<CarouselPrevious className="left-0 bg-background/80 backdrop-blur-sm" />
<CarouselNext className="right-0 bg-background/80 backdrop-blur-sm" />
</Carousel>
{/* Dot Indicators */}
<div className="absolute -bottom-6 left-0 right-0">
<div className="flex justify-center gap-1">
{providers.map((_, index) => (
<button
type="button"
key={index}
className={cn(
'h-1.5 rounded-full transition-all',
index === currentSlide
? 'w-4 bg-primary'
: 'w-1.5 bg-primary/50'
)}
onClick={() => api?.scrollTo(index)}
>
<span className="sr-only">Go to image {index + 1}</span>
</button>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,195 @@
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { AlertCircle, Download, ImageIcon, Share } from 'lucide-react';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { imageHelpers } from '../lib/image-helpers';
import type { ProviderTiming } from '../lib/image-types';
import { Stopwatch } from './Stopwatch';
interface ImageDisplayProps {
provider: string;
image: string | null | undefined;
timing?: ProviderTiming;
failed?: boolean;
fallbackIcon?: React.ReactNode;
enabled?: boolean;
modelId: string;
}
export function ImageDisplay({
provider,
image,
timing,
failed,
fallbackIcon,
modelId,
}: ImageDisplayProps) {
const [isZoomed, setIsZoomed] = useState(false);
useEffect(() => {
if (isZoomed) {
window.history.pushState({ zoomed: true }, '');
}
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isZoomed) {
setIsZoomed(false);
}
};
const handlePopState = () => {
if (isZoomed) {
setIsZoomed(false);
}
};
if (isZoomed) {
document.addEventListener('keydown', handleEscape);
window.addEventListener('popstate', handlePopState);
}
return () => {
document.removeEventListener('keydown', handleEscape);
window.removeEventListener('popstate', handlePopState);
};
}, [isZoomed]);
const handleImageClick = (e: React.MouseEvent) => {
if (image) {
e.stopPropagation();
setIsZoomed(true);
}
};
const handleActionClick = (
e: React.MouseEvent,
imageData: string,
provider: string
) => {
e.stopPropagation();
imageHelpers.shareOrDownload(imageData, provider).catch((error) => {
console.error('Failed to share/download image:', error);
});
};
return (
<>
<div
className={cn(
'relative w-full aspect-square group bg-zinc-50 rounded-lg',
image && !failed && 'cursor-pointer',
(!image || failed) && 'border-1 border-zinc-100'
)}
onClick={handleImageClick}
>
{(image || failed) && (
<div className="absolute top-2 left-2 max-w-[75%] bg-white/95 px-2 py-1 flex items-center gap-2 rounded-lg">
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Label className="text-xs text-gray-900 truncate min-w-0 grow">
{imageHelpers.formatModelId(modelId)}
</Label>
</TooltipTrigger>
<TooltipContent>
<p>{modelId}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{image && !failed ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`data:image/png;base64,${image}`}
alt={`Generated by ${provider}`}
className="w-full h-full object-cover rounded-lg"
/>
<Button
size="icon"
variant="secondary"
className="absolute bottom-2 left-2 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
onClick={(e) => handleActionClick(e, image, provider)}
>
<span className="sm:hidden">
<Share className="h-4 w-4" />
</span>
<span className="hidden sm:block">
<Download className="h-4 w-4" />
</span>
</Button>
{timing?.elapsed && (
<div className="absolute bottom-2 right-2 bg-black/70 backdrop-blur-sm rounded-md px-2 py-1 shadow">
<span className="text-xs text-white/90 font-medium">
{(timing.elapsed / 1000).toFixed(1)}s
</span>
</div>
)}
</>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center">
{failed ? (
fallbackIcon || <AlertCircle className="h-8 w-8 text-red-500" />
) : image ? (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`data:image/png;base64,${image}`}
alt={`Generated by ${provider}`}
className="w-full h-full object-cover rounded-lg"
/>
<Button
size="icon"
variant="secondary"
className="absolute bottom-2 left-2 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
onClick={(e) => handleActionClick(e, image, provider)}
>
<span className="sm:hidden">
<Share className="h-4 w-4" />
</span>
<span className="hidden sm:block">
<Download className="h-4 w-4" />
</span>
</Button>
</>
) : timing?.startTime ? (
<>
{/* <div className="text-zinc-400 mb-2">{provider}</div> */}
<Stopwatch startTime={timing.startTime} />
</>
) : (
<ImageIcon className="h-12 w-12 text-zinc-300" />
)}
</div>
)}
</div>
{isZoomed &&
image &&
createPortal(
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center cursor-pointer min-h-[100dvh] w-screen"
onClick={() => setIsZoomed(false)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`data:image/png;base64,${image}`}
alt={`Generated by ${provider}`}
className="max-h-[90dvh] max-w-[90vw] object-contain"
onClick={(e) => e.stopPropagation()}
/>
</div>,
document.body
)}
</>
);
}

View File

@ -0,0 +1,120 @@
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { AlertCircle, ChevronDown, Settings } from 'lucide-react';
import type {
GeneratedImage,
ImageError,
ProviderTiming,
} from '../lib/image-types';
import {
PROVIDER_ORDER,
type ProviderKey,
initializeProviderRecord,
} from '../lib/provider-config';
import { ImageCarousel } from './ImageCarousel';
import { ImageDisplay } from './ImageDisplay';
interface ImageGeneratorProps {
images: GeneratedImage[];
errors: ImageError[];
failedProviders: ProviderKey[];
timings: Record<ProviderKey, ProviderTiming>;
enabledProviders: Record<ProviderKey, boolean>;
toggleView: () => void;
}
export function ImageGenerator({
images,
errors,
failedProviders,
timings,
enabledProviders,
toggleView,
}: ImageGeneratorProps) {
return (
<div className="space-y-6">
{/* If there are errors, render a collapsible alert */}
{errors.length > 0 && (
<Collapsible>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="flex items-center gap-2 text-destructive"
>
<AlertCircle className="h-4 w-4" />
{errors.length} {errors.length === 1 ? 'error' : 'errors'}{' '}
occurred
<ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-2 mt-2">
{errors.map((err, index) => (
<Alert key={index} variant="destructive">
<AlertCircle className="h-4 w-4" />
<div className="ml-3">
<AlertTitle className="capitalize">
{err.provider} Error
</AlertTitle>
<AlertDescription className="mt-1 text-sm">
{err.message}
</AlertDescription>
</div>
</Alert>
))}
</div>
</CollapsibleContent>
</Collapsible>
)}
<div className="flex items-center justify-between">
<h3 className="text-xl font-semibold">Generated Images</h3>
<Button
variant="outline"
className=""
onClick={() => toggleView()}
size="icon"
>
<Settings className="h-4 w-4" />
</Button>
</div>
{/* Mobile layout: Carousel */}
<div className="sm:hidden">
<ImageCarousel
providers={PROVIDER_ORDER}
images={images}
timings={timings}
failedProviders={failedProviders}
enabledProviders={enabledProviders}
providerToModel={initializeProviderRecord<string>()}
/>
</div>
{/* Desktop layout: Grid */}
<div className="hidden sm:grid sm:grid-cols-2 2xl:grid-cols-4 gap-6">
{PROVIDER_ORDER.map((provider) => {
const imageItem = images.find((img) => img.provider === provider);
const imageData = imageItem?.image;
const timing = timings[provider];
return (
<ImageDisplay
key={provider}
provider={provider}
image={imageData}
timing={timing}
failed={failedProviders.includes(provider)}
enabled={enabledProviders[provider]}
modelId={imageItem?.modelId ?? ''}
/>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
import { Button } from '@/components/ui/button';
import { ArrowUpRightIcon } from 'lucide-react';
import Link from 'next/link';
import { QualityModeToggle } from './QualityModeToggle';
export const ImageGeneratorHeader = () => {
return (
<header className="mb-4">
<div className="mx-auto flex justify-between items-center">
<div>
<h1 className="text-xl flex sm:text-xl sm:font-bold antialiased font-semibold">
<span className="mr-2">🏞</span> AI Image Generator
</h1>
</div>
{/* <Link href={`${process.env.NEXT_PUBLIC_APP_URL}`} target="_blank">
<Button size="icon" className="block sm:hidden">
<ArrowUpRightIcon className="w-4 h-4" />
</Button>
</Link> */}
{/* <QualityModeToggle onValueChange={() => {}} value="performance" /> */}
</div>
</header>
);
};

View File

@ -0,0 +1,144 @@
'use client';
import { useState } from 'react';
import { useImageGeneration } from '../hooks/use-image-generation';
import {
MODEL_CONFIGS,
type ModelMode,
PROVIDERS,
PROVIDER_ORDER,
type ProviderKey,
initializeProviderRecord,
} from '../lib/provider-config';
import type { Suggestion } from '../lib/suggestions';
import { ImageGeneratorHeader } from './ImageGeneratorHeader';
import { ModelCardCarousel } from './ModelCardCarousel';
import { ModelSelect } from './ModelSelect';
import { PromptInput } from './PromptInput';
export function ImagePlayground({
suggestions,
}: {
suggestions: Suggestion[];
}) {
const {
images,
timings,
failedProviders,
isLoading,
startGeneration,
activePrompt,
} = useImageGeneration();
const [showProviders, setShowProviders] = useState(true);
const [selectedModels, setSelectedModels] = useState<
Record<ProviderKey, string>
>(MODEL_CONFIGS.performance);
const [enabledProviders, setEnabledProviders] = useState(
initializeProviderRecord(true)
);
const [mode, setMode] = useState<ModelMode>('performance');
const toggleView = () => {
setShowProviders((prev) => !prev);
};
const handleModeChange = (newMode: ModelMode) => {
setMode(newMode);
setSelectedModels(MODEL_CONFIGS[newMode]);
setShowProviders(true);
};
const handleModelChange = (providerKey: ProviderKey, model: string) => {
setSelectedModels((prev) => ({ ...prev, [providerKey]: model }));
};
const handleProviderToggle = (provider: string, enabled: boolean) => {
setEnabledProviders((prev) => ({
...prev,
[provider]: enabled,
}));
};
const providerToModel = {
replicate: selectedModels.replicate,
openai: selectedModels.openai,
fireworks: selectedModels.fireworks,
fal: selectedModels.fal,
};
const handlePromptSubmit = (newPrompt: string) => {
const activeProviders = PROVIDER_ORDER.filter((p) => enabledProviders[p]);
if (activeProviders.length > 0) {
startGeneration(newPrompt, activeProviders, providerToModel);
}
setShowProviders(false);
};
return (
<div className="rounded-lg bg-background py-8 px-4 sm:px-6 lg:px-8">
<div className="mx-auto">
{/* header */}
{/* <ImageGeneratorHeader /> */}
{/* input prompt */}
<PromptInput
onSubmit={handlePromptSubmit}
isLoading={isLoading}
showProviders={showProviders}
onToggleProviders={toggleView}
mode={mode}
onModeChange={handleModeChange}
suggestions={suggestions}
/>
{/* models carousel */}
{(() => {
const getModelProps = () =>
(Object.keys(PROVIDERS) as ProviderKey[]).map((key) => {
const provider = PROVIDERS[key];
const imageItem = images.find((img) => img.provider === key);
const imageData = imageItem?.image;
const modelId = imageItem?.modelId ?? 'N/A';
const timing = timings[key];
return {
label: provider.displayName,
models: provider.models,
value: selectedModels[key],
providerKey: key,
onChange: (model: string, providerKey: ProviderKey) =>
handleModelChange(providerKey, model),
iconPath: provider.iconPath,
color: provider.color,
enabled: enabledProviders[key],
onToggle: (enabled: boolean) =>
handleProviderToggle(key, enabled),
image: imageData,
modelId,
timing,
failed: failedProviders.includes(key),
};
});
return (
<>
<div className="md:hidden">
<ModelCardCarousel models={getModelProps()} />
</div>
<div className="hidden md:grid md:grid-cols-2 2xl:grid-cols-4 gap-8">
{getModelProps().map((props) => (
<ModelSelect key={props.label} {...props} />
))}
</div>
{activePrompt && activePrompt.length > 0 && (
<div className="text-center mt-8 text-muted-foreground">
{activePrompt}
</div>
)}
</>
);
})()}
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More