├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── app ├── [id] │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx ├── actions.ts ├── admin │ ├── comment │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── creator │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── email │ │ ├── metadata.ts │ │ └── page.tsx │ ├── feedback │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── galgame │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── log │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── metadata.ts │ ├── page.tsx │ ├── report │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── resource │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── setting │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ └── user │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx ├── api │ ├── admin │ │ ├── comment │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── creator │ │ │ ├── approve │ │ │ │ └── route.ts │ │ │ ├── decline │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── feedback │ │ │ ├── handle │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── galgame │ │ │ └── route.ts │ │ ├── log │ │ │ └── route.ts │ │ ├── mail │ │ │ ├── _send.ts │ │ │ └── route.ts │ │ ├── report │ │ │ ├── handle │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── resource │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── setting │ │ │ ├── redirect │ │ │ │ ├── getRedirectConfig.ts │ │ │ │ └── route.ts │ │ │ └── register │ │ │ │ └── route.ts │ │ ├── stats │ │ │ ├── route.ts │ │ │ └── sum │ │ │ │ └── route.ts │ │ └── user │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ ├── apply │ │ ├── route.ts │ │ └── status │ │ │ └── route.ts │ ├── auth │ │ ├── captcha │ │ │ ├── _utils.ts │ │ │ ├── generate.ts │ │ │ ├── route.ts │ │ │ └── verify.ts │ │ ├── check-temp-token │ │ │ └── route.ts │ │ ├── email-notice │ │ │ ├── disable │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── login │ │ │ └── route.ts │ │ ├── register │ │ │ └── route.ts │ │ ├── send-register-code │ │ │ └── route.ts │ │ └── verify-2fa │ │ │ └── route.ts │ ├── comment │ │ └── route.ts │ ├── edit │ │ ├── _upload.ts │ │ ├── batchTag.ts │ │ ├── create.ts │ │ ├── duplicate │ │ │ └── route.ts │ │ ├── route.ts │ │ └── update.ts │ ├── forgot │ │ ├── one │ │ │ └── route.ts │ │ └── two │ │ │ └── route.ts │ ├── galgame │ │ └── route.ts │ ├── home │ │ ├── random │ │ │ └── route.ts │ │ └── route.ts │ ├── message │ │ ├── all │ │ │ └── route.ts │ │ ├── read │ │ │ └── route.ts │ │ ├── route.ts │ │ └── unread │ │ │ └── route.ts │ ├── patch │ │ ├── banner │ │ │ └── route.ts │ │ ├── comment │ │ │ ├── _helpers.ts │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── like │ │ │ │ └── route.ts │ │ │ ├── markdown │ │ │ │ └── route.ts │ │ │ ├── report │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── delete.ts │ │ ├── favorite │ │ │ └── route.ts │ │ ├── feedback │ │ │ └── route.ts │ │ ├── get.ts │ │ ├── introduction │ │ │ ├── route.ts │ │ │ └── tag │ │ │ │ └── route.ts │ │ ├── resource │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── download │ │ │ │ └── route.ts │ │ │ ├── get.ts │ │ │ ├── like │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ └── route.ts │ ├── resource │ │ └── route.ts │ ├── search │ │ ├── route.ts │ │ └── tag │ │ │ └── route.ts │ ├── tag │ │ ├── all │ │ │ └── route.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── galgame │ │ │ └── route.ts │ │ ├── get.ts │ │ ├── route.ts │ │ └── update.ts │ ├── upload │ │ ├── resource │ │ │ └── route.ts │ │ ├── resourceUtils.ts │ │ ├── video │ │ │ └── route.ts │ │ └── videoUtils.ts │ ├── user │ │ ├── follow │ │ │ ├── follow │ │ │ │ └── route.ts │ │ │ ├── follower │ │ │ │ └── route.ts │ │ │ ├── following │ │ │ │ └── route.ts │ │ │ └── unfollow │ │ │ │ └── route.ts │ │ ├── image │ │ │ ├── _upload.ts │ │ │ └── route.ts │ │ ├── mention │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── profile │ │ │ ├── comment │ │ │ │ └── route.ts │ │ │ ├── favorite │ │ │ │ ├── folder │ │ │ │ │ ├── create.ts │ │ │ │ │ ├── delete.ts │ │ │ │ │ ├── get.ts │ │ │ │ │ ├── patch │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── update.ts │ │ │ │ └── route.ts │ │ │ ├── floating │ │ │ │ └── route.ts │ │ │ └── resource │ │ │ │ └── route.ts │ │ ├── setting │ │ │ ├── 2fa │ │ │ │ ├── disable │ │ │ │ │ └── route.ts │ │ │ │ ├── enable │ │ │ │ │ └── route.ts │ │ │ │ ├── save-secret │ │ │ │ │ └── route.ts │ │ │ │ └── status │ │ │ │ │ └── route.ts │ │ │ ├── _upload.ts │ │ │ ├── avatar │ │ │ │ └── route.ts │ │ │ ├── bio │ │ │ │ └── route.ts │ │ │ ├── email-notice │ │ │ │ └── route.ts │ │ │ ├── email │ │ │ │ └── route.ts │ │ │ ├── password │ │ │ │ └── route.ts │ │ │ ├── send-reset-email-code │ │ │ │ └── route.ts │ │ │ └── username │ │ │ │ └── route.ts │ │ └── status │ │ │ ├── check-in │ │ │ └── route.ts │ │ │ ├── info │ │ │ └── route.ts │ │ │ ├── logout │ │ │ └── route.ts │ │ │ └── route.ts │ └── utils │ │ ├── algorithm.ts │ │ ├── checkBufferSize.ts │ │ ├── createMentionMessage.ts │ │ ├── getNSFWHeader.ts │ │ ├── getRemoteIp.ts │ │ ├── jwt.ts │ │ ├── message.ts │ │ ├── parseQuery.ts │ │ ├── render │ │ ├── markdownToHtml.ts │ │ ├── markdownToHtmlExtend.ts │ │ ├── remarkKunExternalLinks.ts │ │ ├── remarkKunLink.ts │ │ └── remarkKunVideo.ts │ │ ├── sendVerificationCodeEmail.ts │ │ ├── sliceUntilDelimiterFromEnd.ts │ │ ├── verify2FA.ts │ │ ├── verifyKunCaptcha.ts │ │ └── verifyVerificationCode.ts ├── apply │ ├── actions.ts │ ├── metadata.ts │ ├── page.tsx │ ├── pending │ │ ├── metadata.ts │ │ └── page.tsx │ └── success │ │ ├── metadata.ts │ │ └── page.tsx ├── auth │ ├── email-notice │ │ ├── metadata.ts │ │ └── page.tsx │ └── forgot │ │ ├── metadata.ts │ │ └── page.tsx ├── comment │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx ├── doc │ ├── [...slug] │ │ ├── metadata.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── metadata.ts │ └── page.tsx ├── edit │ ├── create │ │ ├── metadata.ts │ │ └── page.tsx │ ├── page.tsx │ └── rewrite │ │ ├── metadata.ts │ │ └── page.tsx ├── error.tsx ├── friend-link │ ├── metadata.ts │ └── page.tsx ├── galgame │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx ├── layout.tsx ├── login │ ├── 2fa │ │ ├── metadata.ts │ │ └── page.tsx │ ├── metadata.ts │ └── page.tsx ├── message │ ├── actions.ts │ ├── follow │ │ ├── metadata.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── mention │ │ ├── metadata.ts │ │ └── page.tsx │ ├── metadata.ts │ ├── notice │ │ ├── metadata.ts │ │ └── page.tsx │ ├── page.tsx │ └── system │ │ ├── metadata.ts │ │ └── page.tsx ├── metadata.ts ├── page.tsx ├── providers.tsx ├── redirect │ ├── metadata.ts │ └── page.tsx ├── register │ ├── metadata.ts │ └── page.tsx ├── resource │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx ├── search │ ├── metadata.ts │ └── page.tsx ├── settings │ └── user │ │ ├── metadata.ts │ │ └── page.tsx ├── tag │ ├── [id] │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx └── user │ ├── [id] │ ├── actions.ts │ ├── comment │ │ ├── actions.ts │ │ └── page.tsx │ ├── favorite │ │ ├── actions.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── metadata.ts │ ├── page.tsx │ └── resource │ │ ├── actions.ts │ │ └── page.tsx │ └── page.tsx ├── components ├── admin │ ├── Navbar.tsx │ ├── Sidebar.tsx │ ├── SidebarContent.tsx │ ├── comment │ │ ├── Card.tsx │ │ ├── CommentEdit.tsx │ │ └── Container.tsx │ ├── creator │ │ ├── ActionButton.tsx │ │ ├── Container.tsx │ │ └── RenderCell.tsx │ ├── email │ │ ├── Container.tsx │ │ ├── EmailPreview.tsx │ │ └── EmailTemplate.tsx │ ├── feedback │ │ ├── Container.tsx │ │ ├── FeedbackCard.tsx │ │ └── FeedbackHandler.tsx │ ├── galgame │ │ ├── Container.tsx │ │ └── RenderCell.tsx │ ├── log │ │ ├── Card.tsx │ │ └── Container.tsx │ ├── report │ │ ├── Container.tsx │ │ ├── ReportCard.tsx │ │ └── ReportHandler.tsx │ ├── resource │ │ ├── Container.tsx │ │ ├── RenderCell.tsx │ │ └── ResourceEdit.tsx │ ├── setting │ │ ├── Container.tsx │ │ ├── DisableRegisterSetting.tsx │ │ └── RedirectSetting.tsx │ ├── stats │ │ ├── KunAdminStatistic.tsx │ │ ├── KunAdminSum.tsx │ │ ├── KunStats.tsx │ │ └── StatsCard.tsx │ └── user │ │ ├── Container.tsx │ │ ├── RenderCell.tsx │ │ ├── UserDelete.tsx │ │ └── UserEdit.tsx ├── apply │ ├── Container.tsx │ └── Success.tsx ├── auth │ ├── email-notice │ │ ├── Container.tsx │ │ └── KunEmailNoticeCard.tsx │ └── forgot │ │ ├── Forgot.tsx │ │ ├── StepOne.tsx │ │ └── StepTwo.tsx ├── comment │ ├── CommentCard.tsx │ ├── Container.tsx │ ├── FilterBar.tsx │ └── _sort.d.ts ├── doc │ ├── BlogHeader.tsx │ ├── Card.tsx │ ├── Header.tsx │ ├── Navigation.tsx │ ├── SideTreeItem.tsx │ ├── Sidebar.tsx │ ├── SidebarContent.tsx │ └── TableOfContents.tsx ├── edit │ ├── VNDB.d.ts │ ├── components │ │ ├── BatchTag.tsx │ │ └── ReleaseDateInput.tsx │ ├── create │ │ ├── AliasInput.tsx │ │ ├── BannerImage.tsx │ │ ├── ContentLimit.tsx │ │ ├── CreatePatch.tsx │ │ ├── PatchIntroduction.tsx │ │ ├── PublishButton.tsx │ │ └── VNDBInput.tsx │ └── rewrite │ │ ├── AliasManager.tsx │ │ ├── ContentLimit.tsx │ │ ├── GameNameInput.tsx │ │ ├── RewritePatch.tsx │ │ ├── RewritePatchBanner.tsx │ │ └── VNDBInput.tsx ├── error │ └── ErrorComponent.tsx ├── friend-link │ └── KunFriendLink.tsx ├── galgame │ ├── Card.tsx │ ├── Container.tsx │ ├── FilterBar.tsx │ └── _sort.d.ts ├── home │ ├── Container.tsx │ ├── NavigationItems.tsx │ ├── PatchCard.tsx │ ├── Statistics.tsx │ ├── carousel │ │ ├── DesktopCard.tsx │ │ ├── KunCarousel.tsx │ │ ├── MobileCard.tsx │ │ ├── RandomGalgameButton.tsx │ │ └── mdx.ts │ └── hero │ │ └── HomeHero.tsx ├── kun │ ├── BackToTop.tsx │ ├── CardStats.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── Loading.tsx │ ├── MasonryGrid.tsx │ ├── NavigationBreadcrumb.tsx │ ├── Null.tsx │ ├── Pagination.tsx │ ├── PatchAttribute.tsx │ ├── Sooner.tsx │ ├── TextDivider.tsx │ ├── auth │ │ ├── CaptchaCanvas.tsx │ │ ├── CaptchaModal.tsx │ │ └── captcha.d.ts │ ├── cropper │ │ ├── KunCropControls.tsx │ │ ├── KunImageCropper.tsx │ │ ├── KunImageCropperModal.tsx │ │ ├── KunImageMosaicModal.tsx │ │ ├── KunImageUploader.tsx │ │ ├── KunMosaicController.tsx │ │ ├── types.d.ts │ │ └── utils.ts │ ├── external-link │ │ └── ExternalLink.tsx │ ├── floating-card │ │ ├── KunAvatar.tsx │ │ ├── KunUser.tsx │ │ ├── KunUserCard.tsx │ │ └── KunUserStatCard.tsx │ ├── icons │ │ ├── Discord.tsx │ │ ├── GitHub.tsx │ │ ├── Markdown.tsx │ │ ├── Microsoft.tsx │ │ ├── R18.tsx │ │ └── Telegram.tsx │ ├── image-viewer │ │ ├── AutoImageViewer.tsx │ │ └── ImageViewer.tsx │ ├── milkdown │ │ ├── DualEditorProvider.tsx │ │ ├── Editor.tsx │ │ ├── EditorProvider.tsx │ │ ├── KunEditorProvider.tsx │ │ ├── codemirror │ │ │ ├── Codemirror.tsx │ │ │ ├── setup.ts │ │ │ └── theme.ts │ │ └── plugins │ │ │ ├── Menu.tsx │ │ │ ├── MenuButton.tsx │ │ │ ├── _buttonList.ts │ │ │ └── components │ │ │ ├── ImageUploadButton.tsx │ │ │ ├── LinkInsertButton.tsx │ │ │ ├── emoji │ │ │ ├── EmojiPicker.tsx │ │ │ └── _isoEmoji.ts │ │ │ ├── link │ │ │ ├── KunLink.tsx │ │ │ ├── LinkInsertButton.tsx │ │ │ └── linkPlugin.tsx │ │ │ ├── mention │ │ │ └── MentionsListDropdown.tsx │ │ │ ├── placeholder │ │ │ └── placeholderPlugin.ts │ │ │ ├── remarkDirective.ts │ │ │ ├── stop-link │ │ │ └── stopLinkPlugin.ts │ │ │ ├── uploader.ts │ │ │ └── video │ │ │ ├── Plyr.tsx │ │ │ ├── VideoInsertButton.tsx │ │ │ └── videoPlugin.tsx │ ├── top-bar │ │ ├── Brand.tsx │ │ ├── KunMobileMenu.tsx │ │ ├── NSFWSwitcher.tsx │ │ ├── NavItem.tsx │ │ ├── Search.tsx │ │ ├── ThemeSwitcher.tsx │ │ ├── TopBar.tsx │ │ ├── User.tsx │ │ ├── UserDropdown.tsx │ │ ├── UserMessageBell.tsx │ │ ├── _SettingsDropdown.tsx │ │ └── user │ │ │ ├── CheckIn.tsx │ │ │ └── Logout.tsx │ ├── utils │ │ └── loli.ts │ └── verification-code │ │ └── Code.tsx ├── login │ ├── 2FA.tsx │ ├── Container.tsx │ └── Login.tsx ├── message │ ├── Container.tsx │ ├── MessageCard.tsx │ └── MessageNav.tsx ├── patch │ ├── comment │ │ ├── CommentContent.tsx │ │ ├── CommentDropdown.tsx │ │ ├── CommentLike.tsx │ │ ├── CommentTab.tsx │ │ ├── Comments.tsx │ │ ├── PublishComment.tsx │ │ └── _scrollIntoComment.ts │ ├── header │ │ ├── Actions.tsx │ │ ├── Container.tsx │ │ ├── EditBanner.tsx │ │ ├── Header.tsx │ │ ├── Info.tsx │ │ ├── Tabs.tsx │ │ ├── Tags.tsx │ │ └── button │ │ │ ├── FeedbackButton.tsx │ │ │ └── favorite │ │ │ ├── FavoriteButton.tsx │ │ │ └── FavoriteModal.tsx │ ├── introduction │ │ ├── Info.tsx │ │ ├── IntroductionTab.tsx │ │ ├── PatchTagSelector.tsx │ │ ├── SearchTag.tsx │ │ ├── Tag.tsx │ │ └── _adjust.scss │ └── resource │ │ ├── DownloadCard.tsx │ │ ├── Resource.tsx │ │ ├── ResourceDownload.tsx │ │ ├── ResourceInfo.tsx │ │ ├── ResourceLike.tsx │ │ ├── ResourceTab.tsx │ │ ├── Tabs.tsx │ │ ├── edit │ │ └── EditResourceDialog.tsx │ │ ├── publish │ │ ├── PublishResource.tsx │ │ ├── ResourceDetailsForm.tsx │ │ ├── ResourceLinksInput.tsx │ │ ├── ResourceSectionSelect.tsx │ │ ├── ResourceTypeSelect.tsx │ │ └── fetchAlistSize.ts │ │ ├── share.d.ts │ │ └── upload │ │ ├── FileDropZone.tsx │ │ ├── FileUploadCard.tsx │ │ └── FileUploadContainer.tsx ├── redirect │ ├── Container.tsx │ ├── CountdownTimer.tsx │ └── KunRedirectCard.tsx ├── register │ └── Register.tsx ├── resource │ ├── Container.tsx │ ├── FilterBar.tsx │ ├── ResourceCard.tsx │ └── _sort.d.ts ├── search │ ├── Card.tsx │ ├── Container.tsx │ ├── Input.tsx │ ├── Option.tsx │ ├── SearchHistory.tsx │ └── Suggestion.tsx ├── settings │ ├── Nav.tsx │ └── user │ │ ├── Avatar.tsx │ │ ├── AvatarCrop.tsx │ │ ├── Bio.tsx │ │ ├── Email.tsx │ │ ├── EmailNotice.tsx │ │ ├── Password.tsx │ │ ├── TwoFactorAuth.tsx │ │ ├── User.tsx │ │ └── Username.tsx ├── tag │ ├── Card.tsx │ ├── Container.tsx │ ├── CreateTagModal.tsx │ ├── SearchTag.tsx │ ├── TagHeader.tsx │ ├── TagList.tsx │ └── detail │ │ ├── Container.tsx │ │ ├── DeleteTagModal.tsx │ │ ├── EditTagModal.tsx │ │ ├── FilterBar.tsx │ │ └── _sort.d.ts └── user │ ├── Activity.tsx │ ├── Profile.tsx │ ├── SelfButton.tsx │ ├── Stats.tsx │ ├── comment │ ├── Card.tsx │ └── Container.tsx │ ├── favorite │ ├── Card.tsx │ ├── Container.tsx │ └── EditFolderModal.tsx │ ├── follow │ ├── Follow.tsx │ ├── Stats.tsx │ └── UserList.tsx │ └── resource │ ├── Card.tsx │ └── Container.tsx ├── config ├── cache.ts ├── config.d.ts ├── friend.json ├── friend.ts ├── moyu-moe.ts ├── redirect.json ├── redis.ts ├── site.ts └── user.ts ├── constants ├── admin.ts ├── api │ └── select.ts ├── captcha.ts ├── doc.ts ├── email │ ├── group-templates.ts │ ├── templates │ │ ├── announcement.ts │ │ └── touchgal.ts │ └── verify-templates.ts ├── galgame.ts ├── history.ts ├── home.ts ├── message.ts ├── resource.ts ├── routes │ ├── constants.ts │ ├── matcher.ts │ └── routes.ts ├── top-bar.ts └── user.ts ├── ecosystem.config.js ├── hooks ├── useConfetti.ts ├── useMounted.ts ├── useResizeObserver.ts └── useWindowSize.ts ├── lib ├── mdx │ ├── CustomMDX.tsx │ ├── directoryTree.ts │ ├── element │ │ ├── KunCode.tsx │ │ ├── KunLink.tsx │ │ ├── KunTable.tsx │ │ └── kunHeading.ts │ ├── getPosts.ts │ └── types.d.ts ├── ondrive.ts ├── redis.ts └── s3.ts ├── middleware.ts ├── middleware ├── _verifyHeaderCookie.ts └── auth.ts ├── migration ├── backup │ ├── _checkFileValid.mjs │ ├── _fetchFileSize.mjs │ ├── _fetchVNDBInfo.mjs │ ├── _fixInvalidUniqueId.mjs │ ├── _migrateAlias.mjs │ ├── _migrateComment.mjs │ ├── _migrateMobileGameLinks.mjs │ ├── _migration.mjs │ ├── _resetMessageLink.mjs │ ├── _saveAlias.mjs │ ├── _syncResourceCategories.mjs │ ├── _updatePatchResourceUpdateTime.mjs │ ├── _updateReleased.mjs │ └── _uploadBanner.mjs ├── backupFavorite.mjs ├── command.sh ├── migrateFavorite.mjs └── resetCommentMessageLink.mjs ├── motion ├── bell.ts ├── card.ts └── sooner.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── posts ├── notice │ ├── common.mdx │ ├── donate.mdx │ ├── download.mdx │ ├── feedback.mdx │ ├── info.mdx │ ├── new.mdx │ ├── privacy.mdx │ ├── register.mdx │ └── world.mdx └── post │ ├── 202502dona.mdx │ ├── 202503dona.mdx │ ├── 202504dona.mdx │ └── 202505dona.mdx ├── prisma ├── index.ts └── schema.prisma ├── public ├── apple-touch-icon.avif ├── favicon.ico ├── favicon.webp ├── null.webp ├── posts │ ├── dev │ │ └── documentation │ │ │ └── banner.avif │ ├── galgame │ │ └── resource │ │ │ └── banner.avif │ ├── kun │ │ ├── moe │ │ │ ├── banner.avif │ │ │ └── kun.webp │ │ └── ren │ │ │ ├── banner.avif │ │ │ ├── ren1.avif │ │ │ └── ren2.avif │ └── notice │ │ ├── about │ │ └── banner.avif │ │ ├── cfmsc │ │ └── banner.avif │ │ ├── creator │ │ └── banner.avif │ │ ├── donate │ │ ├── banner.avif │ │ ├── qr1.avif │ │ ├── qr2.avif │ │ └── qr3.avif │ │ ├── feedback │ │ └── banner.avif │ │ ├── galgame-tutorial │ │ └── banner.avif │ │ ├── moemoepoint │ │ └── banner.avif │ │ ├── open-source │ │ └── banner.avif │ │ ├── paradigm │ │ └── banner.avif │ │ ├── patch-tutorial │ │ ├── banner.avif │ │ ├── image1.avif │ │ ├── image2.avif │ │ ├── image3.avif │ │ ├── image4.avif │ │ ├── image5.avif │ │ ├── image6.avif │ │ ├── image7.avif │ │ └── image8.avif │ │ ├── privacy │ │ └── banner.avif │ │ └── rule │ │ └── banner.avif ├── robots.txt ├── sooner │ ├── あーちゃん.webp │ ├── こじかひわ.webp │ ├── 琥珀.webp │ └── 雪々.webp └── touchgal.avif ├── scripts ├── deployBuild.ts ├── deployInstall.ts ├── dynamic-routes │ ├── getKunDynamicBlog.ts │ └── getKunDynamicPatches.ts ├── generateKunSitemap.ts └── postbuild.ts ├── server ├── cron.ts ├── image │ └── auth │ │ ├── other │ │ ├── 22.webp │ │ ├── 24.webp │ │ ├── 29.webp │ │ ├── 37.webp │ │ ├── 38.webp │ │ ├── 46.webp │ │ ├── 50.webp │ │ ├── 54.webp │ │ ├── 59.webp │ │ ├── 64.webp │ │ ├── 65.webp │ │ ├── 70.webp │ │ └── 71.webp │ │ └── white │ │ ├── 14.webp │ │ ├── 16.webp │ │ ├── 26.webp │ │ ├── 51.webp │ │ ├── 59.webp │ │ ├── 6.webp │ │ ├── 7.webp │ │ ├── 76.webp │ │ ├── 8.webp │ │ └── 9.webp └── tasks │ ├── resetDailyTask.ts │ └── setCleanupTask.ts ├── store ├── _cookie.ts ├── breadcrumb.ts ├── editStore.ts ├── milkdownStore.ts ├── rewriteStore.ts ├── searchStore.ts ├── settingStore.ts └── userStore.ts ├── styles ├── blog.scss ├── editor.scss ├── index.scss ├── prose.scss └── tailwind.scss ├── tailwind.config.js ├── tsconfig.json ├── types ├── api │ ├── admin.d.ts │ ├── comment.d.ts │ ├── galgame.d.ts │ ├── home.d.ts │ ├── message.d.ts │ ├── patch.d.ts │ ├── resource.d.ts │ ├── search.d.ts │ ├── tag.d.ts │ ├── upload.d.ts │ └── user.d.ts ├── response.d.ts └── user.d.ts ├── utils ├── actions │ ├── getNSFWHeader.ts │ ├── safeParseSchema.ts │ └── verifyHeaderCookie.ts ├── cn.ts ├── cookies.ts ├── dataURItoBlob.ts ├── formatDistanceToNow.ts ├── formatNumber.ts ├── kunCopy.ts ├── kunErrorHandler.ts ├── kunFetch.ts ├── lz.ts ├── markdownToText.tsx ├── noIndex.tsx ├── random.ts ├── resizeImage.ts ├── sanitizeFileName.ts ├── throttle.ts ├── time.ts └── validate.ts └── validations ├── admin.ts ├── auth.ts ├── comment.ts ├── dotenv-check.ts ├── edit.ts ├── forgot.ts ├── galgame.ts ├── message.ts ├── patch.ts ├── resource.ts ├── search.ts ├── tag.ts └── user.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # env 39 | .env 40 | .env.production 41 | 42 | # uploads 43 | /uploads 44 | 45 | # backup 46 | /backup 47 | 48 | # sitemap 49 | /public/**/*.xml 50 | 51 | # static markdown 52 | /migration/markdown 53 | /migration/*.txt 54 | /migration/*.json 55 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* 2 | enable-pre-post-scripts=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true, 4 | "semi": false, 5 | "bracketSpacing": true, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "files.insertFinalNewline": true, 9 | "htmlWhitespaceSensitivity": "strict", 10 | "trailingComma": "none", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig", 6 | "esbenp.prettier-vscode", 7 | "mgmcdermott.vscode-language-babel", 8 | "streetsidesoftware.code-spell-checker", 9 | "stylelint.vscode-stylelint", 10 | "syler.sass-indented", 11 | "webben.browserslist" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { setKUNGalgameTask } from '~/server/cron' 4 | 5 | setKUNGalgameTask() 6 | 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | import { getHomeData } from '~/app/api/home/route' 9 | 10 | export const kunGetActions = async () => { 11 | const nsfwEnable = await getNSFWHeader() 12 | const response = await getHomeData(nsfwEnable) 13 | return response 14 | } 15 | -------------------------------------------------------------------------------- /app/admin/comment/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getComment } from '~/app/api/admin/comment/get' 6 | import { adminPaginationSchema } from '~/validations/admin' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getComment(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/comment/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `评论管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/comment/page.tsx: -------------------------------------------------------------------------------- 1 | import { Comment } from '~/components/admin/comment/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/admin/creator/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getAdminCreator } from '~/app/api/admin/creator/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getAdminCreator(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/creator/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `创作者管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/creator/page.tsx: -------------------------------------------------------------------------------- 1 | import { Creator } from '~/components/admin/creator/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 3 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const response = await kunGetActions({ 13 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/email/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `邮件群发 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/email/page.tsx: -------------------------------------------------------------------------------- 1 | import { EmailSetting } from '~/components/admin/email/Container' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const revalidate = 3 6 | 7 | export const metadata: Metadata = kunMetadata 8 | 9 | export default function Kun() { 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /app/admin/feedback/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getFeedback } from '~/app/api/admin/feedback/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getFeedback(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/feedback/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `游戏反馈管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/feedback/page.tsx: -------------------------------------------------------------------------------- 1 | import { Feedback } from '~/components/admin/feedback/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/admin/galgame/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getGalgame } from '~/app/api/admin/galgame/route' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | 9 | export const kunGetActions = async ( 10 | params: z.infer 11 | ) => { 12 | const input = safeParseSchema(adminPaginationSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | 17 | const nsfwEnable = await getNSFWHeader() 18 | 19 | const response = await getGalgame(input, nsfwEnable) 20 | return response 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/galgame/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `Galgame 管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/galgame/page.tsx: -------------------------------------------------------------------------------- 1 | import { Galgame } from '~/components/admin/galgame/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from '~/components/admin/Sidebar' 2 | // import { Navbar } from '~/components/admin/Navbar' 3 | import { kunMetadata } from './metadata' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | interface Props { 9 | children: React.ReactNode 10 | } 11 | 12 | export default function Kun({ children }: Props) { 13 | return ( 14 |
15 | 16 |
17 | {/* */} 18 |
{children}
19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/admin/log/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getLog } from '~/app/api/admin/log/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getLog(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/log/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `管理日志 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/log/page.tsx: -------------------------------------------------------------------------------- 1 | import { Log } from '~/components/admin/log/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/admin/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `管理系统`, 6 | description: `为 ${kunMoyuMoe.titleShort} 定制的管理系统, 可以查看 网站概览 用户总数量, Galgame 总数量, 今日评论数, 日活跃用户, 包括 Galgame 管理, Galgame 资源管理, 用户管理, 创作者管理, 评论管理, 系统设置, 管理日志 等等`, 7 | openGraph: { 8 | title: '管理系统', 9 | description: `为 ${kunMoyuMoe.titleShort} 定制的管理系统, 可以查看 网站概览 用户总数量, Galgame 总数量, 今日评论数, 日活跃用户, 包括 Galgame 管理, Galgame 资源管理, 用户管理, 创作者管理, 评论管理, 系统设置, 管理日志 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '管理系统', 16 | description: `为 ${kunMoyuMoe.titleShort} 定制的管理系统, 可以查看 网站概览 用户总数量, Galgame 总数量, 今日评论数, 日活跃用户, 包括 Galgame 管理, Galgame 资源管理, 用户管理, 创作者管理, 评论管理, 系统设置, 管理日志 等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { KunStats } from '~/components/admin/stats/KunStats' 2 | 3 | export default async function Kun() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/admin/report/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getReport } from '~/app/api/admin/report/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getReport(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/report/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `评论举报管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/report/page.tsx: -------------------------------------------------------------------------------- 1 | import { Report } from '~/components/admin/report/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/admin/resource/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getPatchResource } from '~/app/api/admin/resource/get' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | 9 | export const kunGetActions = async ( 10 | params: z.infer 11 | ) => { 12 | const input = safeParseSchema(adminPaginationSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | 17 | const nsfwEnable = await getNSFWHeader() 18 | 19 | const response = await getPatchResource(input, nsfwEnable) 20 | return response 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/resource/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `Galgame 资源管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/resource/page.tsx: -------------------------------------------------------------------------------- 1 | import { Resource } from '~/components/admin/resource/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/admin/setting/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getRedirectConfig } from '~/app/api/admin/setting/redirect/getRedirectConfig' 4 | import { getDisableRegisterStatus } from '~/app/api/admin/setting/register/route' 5 | 6 | export const kunGetRedirectConfigActions = async () => { 7 | const response = await getRedirectConfig() 8 | return response 9 | } 10 | 11 | export const kunGetDisableRegisterStatusActions = async () => { 12 | const response = await getDisableRegisterStatus() 13 | return response 14 | } 15 | -------------------------------------------------------------------------------- /app/admin/setting/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `系统设置 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/setting/page.tsx: -------------------------------------------------------------------------------- 1 | import { AdminSetting } from '~/components/admin/setting/Container' 2 | import { kunMetadata } from './metadata' 3 | import { 4 | kunGetRedirectConfigActions, 5 | kunGetDisableRegisterStatusActions 6 | } from './actions' 7 | import type { Metadata } from 'next' 8 | 9 | export const revalidate = 3 10 | 11 | export const metadata: Metadata = kunMetadata 12 | 13 | export default async function Kun() { 14 | const setting = await kunGetRedirectConfigActions() 15 | const { disableRegister } = await kunGetDisableRegisterStatusActions() 16 | 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/user/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { adminPaginationSchema } from '~/validations/admin' 6 | import { getUserInfo } from '~/app/api/admin/user/get' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(adminPaginationSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserInfo(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/admin/user/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: `用户管理 - ${kunMoyuMoe.titleShort}` 6 | } 7 | -------------------------------------------------------------------------------- /app/admin/user/page.tsx: -------------------------------------------------------------------------------- 1 | import { User } from '~/components/admin/user/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/api/admin/setting/redirect/getRedirectConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readFile } from 'fs/promises' 3 | import { getKv, setKv } from '~/lib/redis' 4 | import type { AdminRedirectConfig } from '~/types/api/admin' 5 | 6 | const REDIS_KEY = 'admin:config:redirect' 7 | 8 | export const getRedirectConfig = async () => { 9 | const redirectJson = await getKv(REDIS_KEY) 10 | if (redirectJson) { 11 | return JSON.parse(redirectJson) as AdminRedirectConfig 12 | } 13 | 14 | const configPath = path.join(process.cwd(), 'config/redirect.json') 15 | const redirectJsonFile = (await readFile( 16 | configPath, 17 | 'utf8' 18 | )) as unknown as string 19 | await setKv(REDIS_KEY, redirectJsonFile, 86400) 20 | 21 | return JSON.parse(redirectJsonFile) as AdminRedirectConfig 22 | } 23 | -------------------------------------------------------------------------------- /app/api/admin/stats/sum/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { prisma } from '~/prisma/index' 3 | import type { SumData } from '~/types/api/admin' 4 | 5 | export const getSumData = async (): Promise => { 6 | const [ 7 | userCount, 8 | galgameCount, 9 | galgameResourceCount, 10 | galgamePatchResourceCount, 11 | galgameCommentCount 12 | ] = await Promise.all([ 13 | prisma.user.count(), 14 | prisma.patch.count(), 15 | prisma.patch_resource.count({ 16 | where: { section: 'galgame' } 17 | }), 18 | prisma.patch_resource.count({ 19 | where: { section: 'patch' } 20 | }), 21 | prisma.patch_comment.count() 22 | ]) 23 | 24 | return { 25 | userCount, 26 | galgameCount, 27 | galgameResourceCount, 28 | galgamePatchResourceCount, 29 | galgameCommentCount 30 | } 31 | } 32 | 33 | export const GET = async () => { 34 | const data = await getSumData() 35 | return NextResponse.json(data) 36 | } 37 | -------------------------------------------------------------------------------- /app/api/apply/status/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 3 | import { prisma } from '~/prisma/index' 4 | 5 | export const getApplyStatus = async (uid: number) => { 6 | const count = await prisma.patch_resource.count({ 7 | where: { user_id: uid } 8 | }) 9 | const user = await prisma.user.findUnique({ 10 | where: { id: uid } 11 | }) 12 | const role = user?.role ?? 0 13 | 14 | return { count, role } 15 | } 16 | 17 | export const GET = async (req: NextRequest) => { 18 | const payload = await verifyHeaderCookie(req) 19 | 20 | const response = await getApplyStatus(payload?.uid ?? 0) 21 | return NextResponse.json(response) 22 | } 23 | -------------------------------------------------------------------------------- /app/api/auth/captcha/_utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | 4 | export const shuffleKunArray = (array: T[]): T[] => { 5 | const shuffled = [...array] 6 | for (let i = shuffled.length - 1; i > 0; i--) { 7 | const j = Math.floor(Math.random() * (i + 1)) 8 | ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]] 9 | } 10 | return shuffled 11 | } 12 | 13 | export const readImageBase64 = async (filePath: string) => { 14 | const fileBuffer = await fs.readFile(filePath) 15 | return `data:image/${path.extname(filePath).substring(1)};base64,${fileBuffer.toString('base64')}` 16 | } 17 | -------------------------------------------------------------------------------- /app/api/auth/captcha/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { kunParsePostBody } from '~/app/api/utils/parseQuery' 3 | import { generateCaptcha } from './generate' 4 | import { verifyCaptcha } from './verify' 5 | import { captchaSchema } from '~/validations/auth' 6 | 7 | export const GET = async () => { 8 | const captcha = await generateCaptcha() 9 | return NextResponse.json(captcha) 10 | } 11 | 12 | export const POST = async (req: NextRequest) => { 13 | const input = await kunParsePostBody(req, captchaSchema) 14 | if (typeof input === 'string') { 15 | return NextResponse.json(input) 16 | } 17 | 18 | const captcha = await verifyCaptcha(input.sessionId, input.selectedIds) 19 | return NextResponse.json(captcha) 20 | } 21 | -------------------------------------------------------------------------------- /app/api/auth/check-temp-token/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { parseCookies } from '~/utils/cookies' 3 | import { verify2FA } from '~/app/api/utils/verify2FA' 4 | 5 | export const GET = async (req: NextRequest) => { 6 | const tempToken = parseCookies(req.headers.get('cookie') ?? '')[ 7 | 'kun-galgame-patch-moe-2fa-token' 8 | ] 9 | if (!tempToken) { 10 | return NextResponse.json('未找到临时令牌') 11 | } 12 | 13 | const payload = verify2FA(tempToken) 14 | if (!payload) { 15 | return NextResponse.json('2FA 临时令牌已过期, 时效为 10 分钟') 16 | } 17 | 18 | return NextResponse.json(payload) 19 | } 20 | -------------------------------------------------------------------------------- /app/api/auth/email-notice/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | const toggleEmailNotice = async (uid: number) => { 6 | const user = await prisma.user.findUnique({ 7 | where: { id: uid } 8 | }) 9 | if (!user) { 10 | return '未找到用户' 11 | } 12 | 13 | await prisma.user.update({ 14 | where: { id: uid }, 15 | data: { enable_email_notice: !user.enable_email_notice } 16 | }) 17 | return {} 18 | } 19 | 20 | export const POST = async (req: NextRequest) => { 21 | const payload = await verifyHeaderCookie(req) 22 | if (!payload) { 23 | return '用户未登录' 24 | } 25 | 26 | const res = await toggleEmailNotice(payload.uid) 27 | return NextResponse.json(res) 28 | } 29 | -------------------------------------------------------------------------------- /app/api/edit/_upload.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | 3 | import { uploadImageToS3 } from '~/lib/s3' 4 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 5 | 6 | export const uploadPatchBanner = async (image: ArrayBuffer, id: number) => { 7 | const banner = await sharp(image) 8 | .resize(1920, 1080, { 9 | fit: 'inside', 10 | withoutEnlargement: true 11 | }) 12 | .avif({ quality: 60 }) 13 | .toBuffer() 14 | const miniBanner = await sharp(image) 15 | .resize(460, 259, { 16 | fit: 'inside', 17 | withoutEnlargement: true 18 | }) 19 | .avif({ quality: 60 }) 20 | .toBuffer() 21 | 22 | if (!checkBufferSize(miniBanner, 1.007)) { 23 | return '图片体积过大' 24 | } 25 | 26 | const bucketName = `patch/${id}/banner` 27 | 28 | await uploadImageToS3(`${bucketName}/banner.avif`, banner) 29 | await uploadImageToS3(`${bucketName}/banner-mini.avif`, miniBanner) 30 | } 31 | -------------------------------------------------------------------------------- /app/api/edit/duplicate/route.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { kunParseGetQuery } from '~/app/api/utils/parseQuery' 4 | import { prisma } from '~/prisma/index' 5 | import { duplicateSchema } from '~/validations/edit' 6 | 7 | export const duplicate = async (input: z.infer) => { 8 | const patch = await prisma.patch.findFirst({ 9 | where: { vndb_id: input.vndbId } 10 | }) 11 | if (patch) { 12 | return 'VNDB ID 重复, 本游戏已经被发布过了' 13 | } 14 | return {} 15 | } 16 | 17 | export const GET = async (req: NextRequest) => { 18 | const input = kunParseGetQuery(req, duplicateSchema) 19 | if (typeof input === 'string') { 20 | return NextResponse.json(input) 21 | } 22 | 23 | const response = await duplicate(input) 24 | return NextResponse.json(response) 25 | } 26 | -------------------------------------------------------------------------------- /app/api/home/random/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '~/prisma/index' 3 | import { getNSFWHeader } from '~/app/api/utils/getNSFWHeader' 4 | 5 | export const getRandomUniqueId = async ( 6 | nsfwEnable: Record 7 | ) => { 8 | const totalArticles = await prisma.patch.findMany({ 9 | where: nsfwEnable, 10 | select: { unique_id: true } 11 | }) 12 | if (totalArticles.length === 0) { 13 | return '未查询到文章' 14 | } 15 | const uniqueIds = totalArticles.map((a) => a.unique_id) 16 | const randomIndex = Math.floor(Math.random() * uniqueIds.length) 17 | 18 | return { uniqueId: uniqueIds[randomIndex] } 19 | } 20 | 21 | export const GET = async (req: NextRequest) => { 22 | const nsfwEnable = getNSFWHeader(req) 23 | 24 | const response = await getRandomUniqueId(nsfwEnable) 25 | return NextResponse.json(response) 26 | } 27 | -------------------------------------------------------------------------------- /app/api/message/read/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '~/prisma/index' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | export const readMessage = async (uid: number) => { 6 | await prisma.user_message.updateMany({ 7 | where: { recipient_id: uid }, 8 | data: { status: { set: 1 } } 9 | }) 10 | return {} 11 | } 12 | 13 | export const PUT = async (req: NextRequest) => { 14 | const payload = await verifyHeaderCookie(req) 15 | if (!payload) { 16 | return NextResponse.json('用户未登录') 17 | } 18 | 19 | const response = await readMessage(payload.uid) 20 | return NextResponse.json(response) 21 | } 22 | -------------------------------------------------------------------------------- /app/api/message/unread/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '~/prisma/index' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | export const getMessage = async (uid: number) => { 6 | const unread = await prisma.user_message.findFirst({ 7 | where: { recipient_id: uid, status: 0 } 8 | }) 9 | return unread 10 | } 11 | 12 | export const GET = async (req: NextRequest) => { 13 | const payload = await verifyHeaderCookie(req) 14 | if (!payload) { 15 | return NextResponse.json('用户未登录') 16 | } 17 | 18 | const response = await getMessage(payload.uid) 19 | return NextResponse.json(response) 20 | } 21 | -------------------------------------------------------------------------------- /app/api/patch/comment/_helpers.ts: -------------------------------------------------------------------------------- 1 | import { convert } from 'html-to-text' 2 | import type { PatchComment } from '~/types/api/patch' 3 | 4 | export const nestComments = (flatComments: PatchComment[]): PatchComment[] => { 5 | const commentMap: { [key: number]: PatchComment } = {} 6 | 7 | flatComments.forEach((comment) => { 8 | comment.reply = [] 9 | commentMap[comment.id] = { ...comment, quotedContent: null } 10 | }) 11 | 12 | const nestedComments: PatchComment[] = [] 13 | 14 | flatComments.forEach((comment) => { 15 | if (comment.parentId) { 16 | const parentComment = commentMap[comment.parentId] 17 | if (parentComment) { 18 | parentComment.reply.push(comment) 19 | comment.quotedContent = convert( 20 | commentMap[comment.parentId].content 21 | ).slice(0, 107) 22 | comment.quotedUsername = commentMap[comment.parentId].user.name 23 | } 24 | } else { 25 | nestedComments.push(comment) 26 | } 27 | }) 28 | 29 | return nestedComments 30 | } 31 | -------------------------------------------------------------------------------- /app/api/patch/comment/markdown/route.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { kunParseGetQuery } from '~/app/api/utils/parseQuery' 4 | import { prisma } from '~/prisma/index' 5 | 6 | const commentIdSchema = z.object({ 7 | commentId: z.coerce 8 | .number({ message: '评论 ID 必须为数字' }) 9 | .min(1) 10 | .max(9999999) 11 | }) 12 | 13 | export const getCommentMarkdown = async ( 14 | input: z.infer 15 | ) => { 16 | const { commentId } = input 17 | 18 | const comment = await prisma.patch_comment.findUnique({ 19 | where: { id: commentId }, 20 | select: { 21 | content: true 22 | } 23 | }) 24 | 25 | return { content: comment?.content ?? '' } 26 | } 27 | 28 | export const GET = async (req: NextRequest) => { 29 | const input = kunParseGetQuery(req, commentIdSchema) 30 | if (typeof input === 'string') { 31 | return NextResponse.json(input) 32 | } 33 | 34 | const response = await getCommentMarkdown(input) 35 | return NextResponse.json(response) 36 | } 37 | -------------------------------------------------------------------------------- /app/api/patch/comment/update.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { patchCommentUpdateSchema } from '~/validations/patch' 4 | 5 | export const updateComment = async ( 6 | input: z.infer, 7 | uid: number, 8 | userRole: number 9 | ) => { 10 | const { commentId, content } = input 11 | 12 | const comment = await prisma.patch_comment.findUnique({ 13 | where: { id: commentId } 14 | }) 15 | if (!comment) { 16 | return '未找到该评论' 17 | } 18 | const commentUserUid = comment.user_id 19 | if (comment.user_id !== uid && userRole < 3) { 20 | return '您没有权限更改该评论' 21 | } 22 | 23 | await prisma.patch_comment.update({ 24 | where: { id: commentId, user_id: commentUserUid }, 25 | data: { 26 | content, 27 | edit: Date.now().toString() 28 | }, 29 | include: { 30 | user: true, 31 | like_by: { 32 | include: { 33 | user: true 34 | } 35 | } 36 | } 37 | }) 38 | return {} 39 | } 40 | -------------------------------------------------------------------------------- /app/api/patch/resource/download/route.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { kunParsePutBody } from '~/app/api/utils/parseQuery' 4 | import { prisma } from '~/prisma/index' 5 | import { updatePatchResourceStatsSchema } from '~/validations/patch' 6 | 7 | export const downloadStats = async ( 8 | input: z.infer 9 | ) => { 10 | return await prisma.$transaction(async (prisma) => { 11 | await prisma.patch.update({ 12 | where: { id: input.patchId }, 13 | data: { download: { increment: 1 } } 14 | }) 15 | 16 | await prisma.patch_resource.update({ 17 | where: { id: input.resourceId }, 18 | data: { download: { increment: 1 } } 19 | }) 20 | return {} 21 | }) 22 | } 23 | 24 | export const PUT = async (req: NextRequest) => { 25 | const input = await kunParsePutBody(req, updatePatchResourceStatsSchema) 26 | if (typeof input === 'string') { 27 | return NextResponse.json(input) 28 | } 29 | 30 | const response = await downloadStats(input) 31 | return NextResponse.json(response) 32 | } 33 | -------------------------------------------------------------------------------- /app/api/tag/create.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { createTagSchema } from '~/validations/tag' 4 | 5 | export const createTag = async ( 6 | input: z.infer, 7 | uid: number 8 | ) => { 9 | const { name, introduction = '', alias = [] } = input 10 | 11 | const existingTag = await prisma.patch_tag.findFirst({ 12 | where: { 13 | OR: [{ name }, { alias: { has: name } }] 14 | } 15 | }) 16 | if (existingTag) { 17 | return '这个标签已经存在了' 18 | } 19 | 20 | const newTag = await prisma.patch_tag.create({ 21 | data: { 22 | user_id: uid, 23 | name, 24 | introduction, 25 | alias 26 | }, 27 | select: { 28 | id: true, 29 | name: true, 30 | count: true, 31 | alias: true 32 | } 33 | }) 34 | 35 | return newTag 36 | } 37 | -------------------------------------------------------------------------------- /app/api/tag/delete.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | 3 | export const deleteTag = async (tagId: number) => { 4 | const tag = await prisma.patch_tag.findUnique({ 5 | where: { id: tagId } 6 | }) 7 | if (!tag) { 8 | return '未找到对应的标签' 9 | } 10 | 11 | await prisma.patch_tag.delete({ 12 | where: { id: tagId } 13 | }) 14 | 15 | return {} 16 | } 17 | -------------------------------------------------------------------------------- /app/api/tag/get.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { getTagByIdSchema } from '~/validations/tag' 4 | import type { TagDetail } from '~/types/api/tag' 5 | 6 | export const getTagById = async (input: z.infer) => { 7 | const { tagId } = input 8 | 9 | const tag: TagDetail | null = await prisma.patch_tag.findUnique({ 10 | where: { id: tagId }, 11 | select: { 12 | id: true, 13 | name: true, 14 | count: true, 15 | alias: true, 16 | introduction: true, 17 | created: true, 18 | user: { 19 | select: { 20 | id: true, 21 | name: true, 22 | avatar: true 23 | } 24 | } 25 | } 26 | }) 27 | if (!tag) { 28 | return '未找到标签' 29 | } 30 | 31 | return tag 32 | } 33 | -------------------------------------------------------------------------------- /app/api/tag/update.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { updateTagSchema } from '~/validations/tag' 4 | import type { TagDetail } from '~/types/api/tag' 5 | 6 | export const updateTag = async (input: z.infer) => { 7 | const { tagId, name, introduction = '', alias = [] } = input 8 | 9 | const existingTag = await prisma.patch_tag.findFirst({ 10 | where: { 11 | OR: [{ name }, { alias: { has: name } }] 12 | } 13 | }) 14 | if (existingTag && existingTag.id !== tagId) { 15 | return '这个标签已经存在了' 16 | } 17 | 18 | const newTag: TagDetail = await prisma.patch_tag.update({ 19 | where: { id: tagId }, 20 | data: { 21 | name, 22 | introduction, 23 | alias 24 | }, 25 | include: { 26 | user: { 27 | select: { 28 | id: true, 29 | name: true, 30 | avatar: true 31 | } 32 | } 33 | } 34 | }) 35 | 36 | return newTag 37 | } 38 | -------------------------------------------------------------------------------- /app/api/user/image/_upload.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | 3 | import { uploadImageToS3 } from '~/lib/s3' 4 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 5 | 6 | export const uploadIntroductionImage = async ( 7 | name: string, 8 | image: ArrayBuffer, 9 | uid: number 10 | ) => { 11 | const minImage = await sharp(image) 12 | .resize(1920, 1080, { 13 | fit: 'inside', 14 | withoutEnlargement: true 15 | }) 16 | .avif({ quality: 30 }) 17 | .toBuffer() 18 | 19 | if (!checkBufferSize(minImage, 1.007)) { 20 | return '图片体积过大' 21 | } 22 | 23 | const s3Key = `user/image/${uid}/${name}.avif` 24 | 25 | await uploadImageToS3(s3Key, minImage) 26 | } 27 | -------------------------------------------------------------------------------- /app/api/user/mention/search/route.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { kunParseGetQuery } from '~/app/api/utils/parseQuery' 4 | import { prisma } from '~/prisma/index' 5 | import { searchUserSchema } from '~/validations/user' 6 | 7 | export const searchUser = async (input: z.infer) => { 8 | const { query } = input 9 | 10 | const users: KunUser[] = await prisma.user.findMany({ 11 | where: { 12 | name: { contains: query, mode: 'insensitive' } 13 | }, 14 | select: { 15 | id: true, 16 | name: true, 17 | avatar: true 18 | }, 19 | take: 50 20 | }) 21 | 22 | return users 23 | } 24 | 25 | export const GET = async (req: NextRequest) => { 26 | const input = kunParseGetQuery(req, searchUserSchema) 27 | if (typeof input === 'string') { 28 | return NextResponse.json(input) 29 | } 30 | 31 | const response = await searchUser(input) 32 | return NextResponse.json(response) 33 | } 34 | -------------------------------------------------------------------------------- /app/api/user/profile/favorite/folder/delete.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | 4 | const folderIdSchema = z.object({ 5 | folderId: z.coerce.number().min(1).max(9999999) 6 | }) 7 | 8 | export const deleteFolder = async ( 9 | input: z.infer, 10 | uid: number 11 | ) => { 12 | const folder = await prisma.user_patch_favorite_folder.findUnique({ 13 | where: { id: input.folderId } 14 | }) 15 | if (!folder) { 16 | return '未找到该收藏夹' 17 | } 18 | if (folder.user_id !== uid) { 19 | return '您没有权限删除该收藏夹' 20 | } 21 | 22 | await prisma.user_patch_favorite_folder.delete({ 23 | where: { id: input.folderId } 24 | }) 25 | 26 | return {} 27 | } 28 | -------------------------------------------------------------------------------- /app/api/user/profile/favorite/folder/update.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { updateFavoriteFolderSchema } from '~/validations/user' 4 | import type { UserFavoritePatchFolder } from '~/types/api/user' 5 | 6 | export const updateFolder = async ( 7 | input: z.infer, 8 | uid: number 9 | ) => { 10 | const folder = await prisma.user_patch_favorite_folder.update({ 11 | where: { id: input.folderId, user_id: uid }, 12 | data: { 13 | name: input.name, 14 | description: input.description, 15 | is_public: input.isPublic 16 | }, 17 | include: { 18 | _count: { 19 | select: { patch: true } 20 | } 21 | } 22 | }) 23 | 24 | const response: UserFavoritePatchFolder = { 25 | name: folder.name, 26 | id: folder.id, 27 | description: folder.description, 28 | is_public: folder.is_public, 29 | isAdd: false, 30 | _count: folder._count 31 | } 32 | 33 | return response 34 | } 35 | -------------------------------------------------------------------------------- /app/api/user/setting/2fa/disable/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | const disable2FA = async (uid: number) => { 6 | await prisma.user.update({ 7 | where: { id: uid }, 8 | data: { 9 | enable_2fa: false, 10 | two_factor_secret: '', 11 | two_factor_backup: [] 12 | } 13 | }) 14 | 15 | return { success: true, message: '2FA 已禁用' } 16 | } 17 | 18 | export const POST = async (req: NextRequest) => { 19 | const payload = await verifyHeaderCookie(req) 20 | if (!payload) { 21 | return NextResponse.json('用户未登录') 22 | } 23 | 24 | const result = await disable2FA(payload.uid) 25 | return NextResponse.json(result) 26 | } 27 | -------------------------------------------------------------------------------- /app/api/user/setting/2fa/save-secret/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | import { kunParsePostBody } from '~/app/api/utils/parseQuery' 5 | import { saveUser2FASecretSchema } from '~/validations/user' 6 | 7 | const saveSecret = async (uid: number, secret: string) => { 8 | await prisma.user.update({ 9 | where: { id: uid }, 10 | data: { 11 | two_factor_secret: secret, 12 | enable_2fa: false 13 | } 14 | }) 15 | 16 | return { success: true } 17 | } 18 | 19 | export const POST = async (req: NextRequest) => { 20 | const input = await kunParsePostBody(req, saveUser2FASecretSchema) 21 | if (typeof input === 'string') { 22 | return NextResponse.json(input) 23 | } 24 | const payload = await verifyHeaderCookie(req) 25 | if (!payload) { 26 | return NextResponse.json('用户未登录') 27 | } 28 | 29 | const result = await saveSecret(payload.uid, input.secret) 30 | return NextResponse.json(result) 31 | } 32 | -------------------------------------------------------------------------------- /app/api/user/setting/2fa/status/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | const get2FAStatus = async (uid?: number) => { 6 | if (!uid) { 7 | return { enabled: false, hasSecret: false } 8 | } 9 | 10 | const user = await prisma.user.findUnique({ 11 | where: { id: uid }, 12 | select: { 13 | enable_2fa: true, 14 | two_factor_secret: true, 15 | two_factor_backup: true 16 | } 17 | }) 18 | 19 | return { 20 | enabled: user?.enable_2fa || false, 21 | hasSecret: !!user?.two_factor_secret, 22 | backupCodeLength: user?.two_factor_backup 23 | ? user.two_factor_backup.length 24 | : 0 25 | } 26 | } 27 | 28 | export const GET = async (req: NextRequest) => { 29 | const payload = await verifyHeaderCookie(req) 30 | const result = await get2FAStatus(payload?.uid) 31 | return NextResponse.json(result) 32 | } 33 | -------------------------------------------------------------------------------- /app/api/user/setting/_upload.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | 3 | import { uploadImageToS3 } from '~/lib/s3' 4 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 5 | 6 | export const uploadUserAvatar = async (image: ArrayBuffer, uid: number) => { 7 | const avatar = await sharp(image) 8 | .resize(256, 256, { 9 | fit: 'inside', 10 | withoutEnlargement: true 11 | }) 12 | .avif({ quality: 60 }) 13 | .toBuffer() 14 | const miniAvatar = await sharp(image) 15 | .resize(100, 100, { 16 | fit: 'inside', 17 | withoutEnlargement: true 18 | }) 19 | .avif({ quality: 50 }) 20 | .toBuffer() 21 | 22 | if (!checkBufferSize(avatar, 1.007)) { 23 | return '图片体积过大' 24 | } 25 | 26 | const bucketName = `user/avatar/user_${uid}` 27 | 28 | await uploadImageToS3(`${bucketName}/avatar.avif`, avatar) 29 | await uploadImageToS3(`${bucketName}/avatar-mini.avif`, miniAvatar) 30 | } 31 | -------------------------------------------------------------------------------- /app/api/user/setting/bio/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { kunParsePostBody } from '~/app/api/utils/parseQuery' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | import { prisma } from '~/prisma/index' 5 | import { bioSchema } from '~/validations/user' 6 | 7 | export const POST = async (req: NextRequest) => { 8 | const input = await kunParsePostBody(req, bioSchema) 9 | if (typeof input === 'string') { 10 | return NextResponse.json(input) 11 | } 12 | const payload = await verifyHeaderCookie(req) 13 | if (!payload) { 14 | return NextResponse.json('用户未登录') 15 | } 16 | 17 | await prisma.user.update({ 18 | where: { id: payload.uid }, 19 | data: { bio: input.bio } 20 | }) 21 | 22 | return NextResponse.json({}) 23 | } 24 | -------------------------------------------------------------------------------- /app/api/user/setting/email-notice/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | 5 | const toggleEmailNotice = async (uid: number) => { 6 | const user = await prisma.user.findUnique({ 7 | where: { id: uid } 8 | }) 9 | if (user === null) { 10 | return '未找到用户' 11 | } 12 | 13 | await prisma.user.update({ 14 | where: { id: uid }, 15 | data: { enable_email_notice: !user.enable_email_notice } 16 | }) 17 | return {} 18 | } 19 | 20 | export const POST = async (req: NextRequest) => { 21 | const payload = await verifyHeaderCookie(req) 22 | if (!payload) { 23 | return '用户未登录' 24 | } 25 | 26 | const res = await toggleEmailNotice(payload.uid) 27 | return NextResponse.json(res) 28 | } 29 | -------------------------------------------------------------------------------- /app/api/user/status/check-in/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { prisma } from '~/prisma/index' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | import { randomNum } from '~/utils/random' 5 | 6 | const checkIn = async (uid: number) => { 7 | const user = await prisma.user.findUnique({ 8 | where: { id: uid } 9 | }) 10 | if (!user) { 11 | return '用户未找到' 12 | } 13 | if (user.daily_check_in) { 14 | return '您今天已经签到过了' 15 | } 16 | 17 | const randomMoemoepoints = randomNum(0, 7) 18 | 19 | await prisma.user.update({ 20 | where: { id: uid }, 21 | data: { 22 | moemoepoint: { increment: randomMoemoepoints }, 23 | daily_check_in: { set: 1 } 24 | } 25 | }) 26 | 27 | return { randomMoemoepoints } 28 | } 29 | 30 | export async function POST(req: NextRequest) { 31 | const payload = await verifyHeaderCookie(req) 32 | if (!payload) { 33 | return NextResponse.json('用户未登录') 34 | } 35 | 36 | const res = await checkIn(payload.uid) 37 | return NextResponse.json(res) 38 | } 39 | -------------------------------------------------------------------------------- /app/api/user/status/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { cookies } from 'next/headers' 3 | import { verifyHeaderCookie } from '~/middleware/_verifyHeaderCookie' 4 | import { deleteKunToken } from '~/app/api/utils/jwt' 5 | 6 | export async function POST(req: NextRequest) { 7 | const payload = await verifyHeaderCookie(req) 8 | if (!payload) { 9 | return NextResponse.json('用户未登录') 10 | } 11 | 12 | await deleteKunToken(payload.uid) 13 | const cookie = await cookies() 14 | cookie.delete('kun-galgame-patch-moe-token') 15 | 16 | return NextResponse.json({ message: '退出登录成功' }) 17 | } 18 | -------------------------------------------------------------------------------- /app/api/utils/checkBufferSize.ts: -------------------------------------------------------------------------------- 1 | export const checkBufferSize = (buffer: Buffer, maxSizeInMegabyte: number) => { 2 | const maxSizeInBytes = maxSizeInMegabyte * 1024 * 1024 3 | return buffer.length <= maxSizeInBytes 4 | } 5 | -------------------------------------------------------------------------------- /app/api/utils/getNSFWHeader.ts: -------------------------------------------------------------------------------- 1 | import { parseCookies } from '~/utils/cookies' 2 | import type { NextRequest } from 'next/server' 3 | 4 | export const getNSFWHeader = (req: NextRequest) => { 5 | const cookies = parseCookies(req.headers.get('cookie') ?? '') 6 | const token = cookies['kun-patch-setting-store|state|data|kunNsfwEnable'] 7 | if (!token) { 8 | return { content_limit: 'sfw' } 9 | } 10 | 11 | if (token === 'all') { 12 | return {} 13 | } else { 14 | return { content_limit: token } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/api/utils/getRemoteIp.ts: -------------------------------------------------------------------------------- 1 | export const getRemoteIp = (headers: Headers): string => { 2 | const ipForwarded = () => { 3 | const ip = headers.get('x-forwarded-for') 4 | if (Array.isArray(ip)) { 5 | return ip[0] 6 | } else { 7 | return ip?.split(',')[0].trim() 8 | } 9 | } 10 | 11 | const xRealIp = headers.get('x-real-ip') 12 | const cfConnectingIp = headers.get('CF-Connecting-IP') 13 | 14 | return cfConnectingIp || ipForwarded() || xRealIp || '' 15 | } 16 | -------------------------------------------------------------------------------- /app/api/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma/index' 2 | import type { CreateMessageType } from '~/types/api/message' 3 | 4 | export const createMessage = async (data: CreateMessageType) => { 5 | const message = await prisma.user_message.create({ 6 | data 7 | }) 8 | return message 9 | } 10 | 11 | export const createDedupMessage = async (data: CreateMessageType) => { 12 | const duplicatedMessage = await prisma.user_message.findFirst({ 13 | where: { 14 | ...data 15 | } 16 | }) 17 | if (duplicatedMessage) { 18 | return 19 | } 20 | 21 | const message = createMessage(data) 22 | 23 | return message 24 | } 25 | -------------------------------------------------------------------------------- /app/api/utils/render/remarkKunExternalLinks.ts: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | import { toString } from 'hast-util-to-string' 3 | import type { Plugin } from 'unified' 4 | import type { Node } from 'unist' 5 | 6 | export const remarkKunExternalLinks: Plugin<[], Node> = () => { 7 | return (tree) => { 8 | visit(tree, 'element', (node: any) => { 9 | if (node.tagName === 'a') { 10 | const href = node.properties?.href 11 | if (!href) { 12 | return 13 | } 14 | node.properties['data-kun-external-link'] = '' 15 | node.properties['data-href'] = href 16 | node.properties['data-text'] = toString(node) 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/api/utils/render/remarkKunLink.ts: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | import type { Plugin } from 'unified' 3 | import type { Node } from 'unist' 4 | 5 | export const remarkKunLink: Plugin<[], Node> = () => { 6 | return (tree) => { 7 | visit(tree, (node: any) => { 8 | if ( 9 | node.type === 'containerDirective' || 10 | node.type === 'leafDirective' || 11 | node.type === 'textDirective' 12 | ) { 13 | if (node.name !== 'kun-link') return 14 | 15 | const data = node.data || (node.data = {}) 16 | const attributes = node.attributes || {} 17 | 18 | data.hName = 'div' 19 | data.hProperties = { 20 | 'data-kun-link': '', 21 | 'data-href': attributes.href, 22 | 'data-text': attributes.text, 23 | className: 'w-full' 24 | } 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/api/utils/render/remarkKunVideo.ts: -------------------------------------------------------------------------------- 1 | import { visit } from 'unist-util-visit' 2 | import type { Plugin } from 'unified' 3 | import type { Node } from 'unist' 4 | 5 | export const remarkKunVideo: Plugin<[], Node> = () => { 6 | return (tree) => { 7 | visit(tree, (node: any) => { 8 | if ( 9 | node.type === 'containerDirective' || 10 | node.type === 'leafDirective' || 11 | node.type === 'textDirective' 12 | ) { 13 | if (node.name !== 'kun-video') return 14 | 15 | const data = node.data || (node.data = {}) 16 | const attributes = node.attributes || {} 17 | 18 | data.hName = 'div' 19 | data.hProperties = { 20 | 'data-video-player': '', 21 | 'data-src': attributes.src, 22 | className: 'w-full my-4 overflow-hidden shadow-lg rounded-xl' 23 | } 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/api/utils/sliceUntilDelimiterFromEnd.ts: -------------------------------------------------------------------------------- 1 | export const sliceUntilDelimiterFromEnd = ( 2 | input: string = '', 3 | delimiter: string = '\n\n' 4 | ) => { 5 | const lastDelimiterIndex = input.lastIndexOf(delimiter) 6 | if (lastDelimiterIndex === -1) { 7 | return input 8 | } 9 | return input.slice(lastDelimiterIndex + delimiter.length) 10 | } 11 | -------------------------------------------------------------------------------- /app/api/utils/verify2FA.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import type { KunGalgameStatelessPayload } from '~/app/api/utils/jwt' 3 | 4 | export const verify2FA = (token: string) => { 5 | try { 6 | const payload = jwt.verify( 7 | token, 8 | process.env.JWT_SECRET! 9 | ) as KunGalgameStatelessPayload 10 | 11 | if (!payload.require2FA) { 12 | return null 13 | } 14 | 15 | return payload 16 | } catch (error) { 17 | return null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/api/utils/verifyVerificationCode.ts: -------------------------------------------------------------------------------- 1 | import { getKv } from '~/lib/redis' 2 | 3 | export const verifyVerificationCode = async ( 4 | email: string, 5 | userProvidedCode: string 6 | ): Promise => { 7 | const storedCode = await getKv(email) 8 | 9 | if (!storedCode) { 10 | return false 11 | } 12 | 13 | return userProvidedCode === storedCode 14 | } 15 | -------------------------------------------------------------------------------- /app/apply/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 4 | import { getApplyStatus } from '~/app/api/apply/status/route' 5 | 6 | export const kunGetActions = async () => { 7 | const payload = await verifyHeaderCookie() 8 | 9 | const response = await getApplyStatus(payload?.uid ?? 0) 10 | return response 11 | } 12 | -------------------------------------------------------------------------------- /app/apply/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '申请成为创作者', 6 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储`, 7 | openGraph: { 8 | title: '申请成为创作者', 9 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '申请成为创作者', 16 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/apply` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/apply/page.tsx: -------------------------------------------------------------------------------- 1 | import { ApplyContainer } from '~/components/apply/Container' 2 | import { redirect } from 'next/navigation' 3 | import { kunMetadata } from './metadata' 4 | import { kunGetActions } from './actions' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 3 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const { count, role } = await kunGetActions() 13 | 14 | if (role > 1) { 15 | redirect('/apply/success') 16 | } 17 | 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /app/apply/pending/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '申请已经提交 | 审核中', 6 | description: `感谢您申请成为创作者! 我们会在数小时内审核您的请求! 创作者请求正在审核中`, 7 | openGraph: { 8 | title: '申请已经提交 | 审核中', 9 | description: `感谢您申请成为创作者! 我们会在数小时内审核您的请求! 创作者请求正在审核中`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '申请已经提交 | 审核中', 16 | description: `感谢您申请成为创作者! 我们会在数小时内审核您的请求! 创作者请求正在审核中` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/apply` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/apply/pending/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from '@nextui-org/card' 2 | import { Button } from '@nextui-org/button' 3 | import Link from 'next/link' 4 | import { CheckCircle2 } from 'lucide-react' 5 | import { kunMetadata } from './metadata' 6 | import type { Metadata } from 'next' 7 | 8 | export const metadata: Metadata = kunMetadata 9 | 10 | export default function Kun() { 11 | return ( 12 | 13 |
14 | 15 |
16 |

申请已经提交

17 |
18 |

感谢您申请成为创作者!

19 |

我们会在数小时内审核您的请求!

20 |
21 | 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/apply/success/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '申请成为创作者', 6 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储`, 7 | openGraph: { 8 | title: '申请成为创作者', 9 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '申请成为创作者', 16 | description: `申请成为创作者以获得使用本站存储的权限, 自由的发布 Galgame 资源, 使用最快最先进的 S3 对象存储` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/apply` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/apply/success/page.tsx: -------------------------------------------------------------------------------- 1 | import { ApplySuccess } from '~/components/apply/Success' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Kun() { 8 | return ( 9 |
10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/auth/email-notice/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '退订邮件通知', 6 | description: `您可以在此处退订网站的邮件通知`, 7 | openGraph: { 8 | title: '退订邮件通知', 9 | description: `您可以在此处退订网站的邮件通知`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '退订邮件通知', 16 | description: `您可以在此处退订网站的邮件通知` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/auth/email-notice` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/auth/email-notice/page.tsx: -------------------------------------------------------------------------------- 1 | import { KunContainer } from '~/components/auth/email-notice/Container' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Kun() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/auth/forgot/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '忘记密码 | 找回密码', 6 | description: `忘记了您的 ${kunMoyuMoe.titleShort} 账号, 您可以通过邮箱, 用户找回密码和账号`, 7 | openGraph: { 8 | title: '忘记密码 | 找回密码', 9 | description: `忘记了您的 ${kunMoyuMoe.titleShort} 账号, 您可以通过邮箱, 用户找回密码和账号`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '忘记密码 | 找回密码', 16 | description: `忘记了您的 ${kunMoyuMoe.titleShort} 账号, 您可以通过邮箱, 用户找回密码和账号` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/auth/forgot` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/auth/forgot/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPassword } from '~/components/auth/forgot/Forgot' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Kun() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/comment/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { commentSchema } from '~/validations/comment' 6 | import { getComment } from '~/app/api/comment/route' 7 | 8 | export const kunGetActions = async (params: z.infer) => { 9 | const input = safeParseSchema(commentSchema, params) 10 | if (typeof input === 'string') { 11 | return input 12 | } 13 | 14 | const response = await getComment(input) 15 | return response 16 | } 17 | -------------------------------------------------------------------------------- /app/comment/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: 'Galgame 评论', 6 | description: `最新发布的 Galgame 评论列表, 包括对下载的看法, 对 Galgame 的评分, 对 Galgame 的吐槽等等`, 7 | openGraph: { 8 | title: 'Galgame 评论', 9 | description: `最新发布的 Galgame 评论列表, 包括对下载的看法, 对 Galgame 的评分, 对 Galgame 的吐槽等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: 'Galgame 评论', 16 | description: `最新发布的 Galgame 评论列表, 包括对下载的看法, 对 Galgame 的评分, 对 Galgame 的吐槽等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/comment` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/comment/page.tsx: -------------------------------------------------------------------------------- 1 | import { CardContainer } from '~/components/comment/Container' 2 | import { kunMetadata } from './metadata' 3 | import { Suspense } from 'react' 4 | import { kunGetActions } from './actions' 5 | import { ErrorComponent } from '~/components/error/ErrorComponent' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | interface Props { 13 | searchParams?: Promise<{ page?: number }> 14 | } 15 | 16 | export default async function Kun({ searchParams }: Props) { 17 | const res = await searchParams 18 | const currentPage = res?.page ? res.page : 1 19 | 20 | const response = await kunGetActions({ 21 | sortField: 'created', 22 | sortOrder: 'desc', 23 | page: currentPage, 24 | limit: 50 25 | }) 26 | if (typeof response === 'string') { 27 | return 28 | } 29 | 30 | return ( 31 | 32 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/doc/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { KunSidebar } from '~/components/doc/Sidebar' 3 | import { getDirectoryTree } from '~/lib/mdx/directoryTree' 4 | 5 | interface LayoutProps { 6 | children: ReactNode 7 | } 8 | 9 | export default function Layout({ children }: LayoutProps) { 10 | const tree = getDirectoryTree() 11 | 12 | return ( 13 |
14 | 15 |
{children}
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/doc/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '帮助文档 | 网站博客', 6 | description: `${kunMoyuMoe.titleShort} 是一个非盈利的, 由社区驱动的, 完全开源免费的 Galgame 资源下载网站。它由现代框架 Next.js 驱动, 为保证最好的性能, 恳请各位朋友提出宝贵的意见`, 7 | openGraph: { 8 | title: '帮助文档 | 网站博客', 9 | description: `${kunMoyuMoe.titleShort} 是一个非盈利的, 由社区驱动的, 完全开源免费的 Galgame 资源下载网站。它由现代框架 Next.js 驱动, 为保证最好的性能, 恳请各位朋友提出宝贵的意见`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '帮助文档 | 网站博客', 16 | description: `${kunMoyuMoe.titleShort} 是一个非盈利的, 由社区驱动的, 完全开源免费的 Galgame 资源下载网站。它由现代框架 Next.js 驱动, 为保证最好的性能, 恳请各位朋友提出宝贵的意见` 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/edit/create/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '发布 Galgame', 6 | description: 7 | '您需要创建一个新 Galgame, 稍后在 Galgame 页面添加下载资源, 如果已经有这个 Galgame 了, 直接在对应页面添加资源即可', 8 | openGraph: { 9 | title: '发布 Galgame', 10 | description: 11 | '您需要创建一个新 Galgame, 稍后在 Galgame 页面添加下载资源, 如果已经有这个 Galgame 了, 直接在对应页面添加资源即可', 12 | type: 'website', 13 | images: kunMoyuMoe.images 14 | }, 15 | twitter: { 16 | card: 'summary_large_image', 17 | title: '发布 Galgame', 18 | description: 19 | '您需要创建一个新 Galgame, 稍后在 Galgame 页面添加下载资源, 如果已经有这个 Galgame 了, 直接在对应页面添加资源即可' 20 | }, 21 | alternates: { 22 | canonical: `${kunMoyuMoe.domain.main}/edit/create` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/edit/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { CreatePatch } from '~/components/edit/create/CreatePatch' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Create() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function Kun() { 4 | redirect('/edit/create') 5 | } 6 | -------------------------------------------------------------------------------- /app/edit/rewrite/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '更改 Galgame', 6 | description: 7 | '更改已经发布的 Galgame 信息, 介绍, 标签, 会社, 别名等, 然后提出 pull request', 8 | openGraph: { 9 | title: '更改 Galgame', 10 | description: 11 | '更改已经发布的 Galgame 信息, 介绍, 标签, 会社, 别名等, 然后提出 pull request', 12 | type: 'website', 13 | images: kunMoyuMoe.images 14 | }, 15 | twitter: { 16 | card: 'summary_large_image', 17 | title: '更改 Galgame', 18 | description: 19 | '更改已经发布的 Galgame 信息, 介绍, 标签, 会社, 别名等, 然后提出 pull request' 20 | }, 21 | alternates: { 22 | canonical: `${kunMoyuMoe.domain.main}/edit/rewrite` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/edit/rewrite/page.tsx: -------------------------------------------------------------------------------- 1 | import { RewritePatch } from '~/components/edit/rewrite/RewritePatch' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default async function Kun() { 8 | return ( 9 |
10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import toast from 'react-hot-toast' 6 | 7 | export default function Error({ 8 | error, 9 | reset 10 | }: { 11 | error: Error & { digest?: string } 12 | reset: () => void 13 | }) { 14 | useEffect(() => { 15 | toast.error(error) 16 | }, [error]) 17 | 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /app/friend-link/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import { kunFriends } from '~/config/friend' 3 | import type { Metadata } from 'next' 4 | import type { KunSiteImage } from '~/config/config' 5 | 6 | const friendName = kunFriends.map((f) => f.name) 7 | 8 | const friendIcons: KunSiteImage[] = kunFriends.map((f) => ({ 9 | url: f.link, 10 | width: 64, 11 | height: 64, 12 | alt: f.label 13 | })) 14 | 15 | export const kunMetadata: Metadata = { 16 | title: '友情链接', 17 | description: `点击以进入 ${friendName}`, 18 | openGraph: { 19 | title: '友情链接', 20 | description: `点击以进入 ${friendName}`, 21 | type: 'website', 22 | images: friendIcons 23 | }, 24 | twitter: { 25 | card: 'summary_large_image', 26 | title: '友情链接', 27 | description: `点击以进入 ${friendName}`, 28 | images: kunMoyuMoe.images 29 | }, 30 | alternates: { 31 | canonical: `${kunMoyuMoe.domain.main}/friend-link` 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/friend-link/page.tsx: -------------------------------------------------------------------------------- 1 | import { KunFriendLink } from '~/components/friend-link/KunFriendLink' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Kun() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/galgame/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { galgameSchema } from '~/validations/galgame' 6 | import { getGalgame } from '~/app/api/galgame/route' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | 9 | export const kunGetActions = async (params: z.infer) => { 10 | const input = safeParseSchema(galgameSchema, params) 11 | if (typeof input === 'string') { 12 | return input 13 | } 14 | 15 | const nsfwEnable = await getNSFWHeader() 16 | 17 | const response = await getGalgame(input, nsfwEnable) 18 | return response 19 | } 20 | -------------------------------------------------------------------------------- /app/login/2fa/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '两步验证' 5 | } 6 | -------------------------------------------------------------------------------- /app/login/2fa/page.tsx: -------------------------------------------------------------------------------- 1 | import { TwoFactor } from '~/components/login/2FA' 2 | import { kunMetadata } from './metadata' 3 | import { LoginContainer } from '~/components/login/Container' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | export default function Kun() { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from '~/components/login/Login' 2 | import { kunMetadata } from './metadata' 3 | import { LoginContainer } from '~/components/login/Container' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | export default function Kun() { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/message/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getMessageSchema } from '~/validations/message' 6 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 7 | import { getMessage } from '~/app/api/message/all/route' 8 | 9 | export const kunGetActions = async ( 10 | params: z.infer 11 | ) => { 12 | const input = safeParseSchema(getMessageSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | const payload = await verifyHeaderCookie() 17 | if (!payload) { 18 | return '用户登陆失效' 19 | } 20 | 21 | const response = await getMessage(input, payload.uid) 22 | return response 23 | } 24 | -------------------------------------------------------------------------------- /app/message/follow/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '关注消息', 6 | description: `这是关注消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户关注的人最新的动态, 最近发布的 Galgame 资源, 发布的 Galgame 等等`, 7 | openGraph: { 8 | title: '关注消息', 9 | description: `这是关注消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户关注的人最新的动态, 最近发布的 Galgame 资源, 发布的 Galgame 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '关注消息', 16 | description: `这是关注消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户关注的人最新的动态, 最近发布的 Galgame 资源, 发布的 Galgame 等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/message/system` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/message/follow/page.tsx: -------------------------------------------------------------------------------- 1 | import { MessageContainer } from '~/components/message/Container' 2 | import { ErrorComponent } from '~/components/error/ErrorComponent' 3 | import { kunMetadata } from './metadata' 4 | import { kunGetActions } from '../actions' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 3 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const response = await kunGetActions({ 13 | type: 'follow', 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/message/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MessageNav } from '~/components/message/MessageNav' 2 | import { KunHeader } from '~/components/kun/Header' 3 | import { kunMetadata } from './metadata' 4 | import { Suspense } from 'react' 5 | import type { Metadata } from 'next' 6 | 7 | export const metadata: Metadata = kunMetadata 8 | 9 | export default function MessageLayout({ 10 | children 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | return ( 15 | 16 |
17 | 21 |
22 | 23 |
{children}
24 |
25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/message/mention/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '@ 消息' 5 | } 6 | -------------------------------------------------------------------------------- /app/message/mention/page.tsx: -------------------------------------------------------------------------------- 1 | import { MessageContainer } from '~/components/message/Container' 2 | import { ErrorComponent } from '~/components/error/ErrorComponent' 3 | import { kunMetadata } from './metadata' 4 | import { kunGetActions } from '../actions' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 5 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const response = await kunGetActions({ 13 | type: 'mention', 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/message/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '用户消息', 6 | description: `这是用户消息页面, 第一次访问对应的页面会自动已读所有消息`, 7 | openGraph: { 8 | title: '用户消息', 9 | description: `这是用户消息页面, 第一次访问对应的页面会自动已读所有消息`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '用户消息', 16 | description: `这是用户消息页面, 第一次访问对应的页面会自动已读所有消息` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/message` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/message/notice/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '通知消息', 6 | description: `这是通知消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户的被赞, 评论, 被回复, 被关注, 关注者, 私信 消息`, 7 | openGraph: { 8 | title: '通知消息', 9 | description: `这是通知消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户的被赞, 评论, 被回复, 被关注, 关注者, 私信 消息`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '通知消息', 16 | description: `这是通知消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 用户的被赞, 评论, 被回复, 被关注, 关注者, 私信 消息` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/message/system` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/message/notice/page.tsx: -------------------------------------------------------------------------------- 1 | import { MessageContainer } from '~/components/message/Container' 2 | import { ErrorComponent } from '~/components/error/ErrorComponent' 3 | import { kunMetadata } from './metadata' 4 | import { kunGetActions } from '../actions' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 3 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const response = await kunGetActions({ page: 1, limit: 30 }) 13 | if (typeof response === 'string') { 14 | return 15 | } 16 | 17 | return ( 18 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/message/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function Kun() { 4 | redirect('/message/notice') 5 | } 6 | -------------------------------------------------------------------------------- /app/message/system/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '系统消息', 6 | description: `这是系统消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 系统发送给全体用户的消息`, 7 | openGraph: { 8 | title: '系统消息', 9 | description: `这是系统消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 系统发送给全体用户的消息`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '系统消息', 16 | description: `这是系统消息页面, 本页面展示了 ${kunMoyuMoe.titleShort} 系统发送给全体用户的消息` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/message/system` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/message/system/page.tsx: -------------------------------------------------------------------------------- 1 | import { MessageContainer } from '~/components/message/Container' 2 | import { ErrorComponent } from '~/components/error/ErrorComponent' 3 | import { kunMetadata } from './metadata' 4 | import { kunGetActions } from '../actions' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 3 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | export default async function Kun() { 12 | const response = await kunGetActions({ 13 | type: 'system', 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { HomeContainer } from '~/components/home/Container' 2 | import { kunGetActions } from './actions' 3 | 4 | export const revalidate = 3 5 | 6 | export default async function Kun() { 7 | const response = await kunGetActions() 8 | 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { AppProgressBar } from 'next-nprogress-bar' 4 | import { NextUIProvider } from '@nextui-org/react' 5 | import { ThemeProvider } from 'next-themes' 6 | import { useRouter } from 'next-nprogress-bar' 7 | 8 | export const Providers = ({ children }: { children: React.ReactNode }) => { 9 | const router = useRouter() 10 | 11 | return ( 12 | 13 | {children} 14 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/redirect/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '外部链接跳转', 6 | description: `外链跳转页面, 解析外链以保证安全`, 7 | openGraph: { 8 | title: '友情链接', 9 | description: `外链跳转页面, 解析外链以保证安全`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '友情链接', 16 | description: `外链跳转页面, 解析外链以保证安全` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/jump` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/redirect/page.tsx: -------------------------------------------------------------------------------- 1 | import { kunMetadata } from './metadata' 2 | import { KunRedirectContainer } from '~/components/redirect/Container' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function Kun() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { kunMetadata } from './metadata' 2 | import { RegisterForm } from '~/components/register/Register' 3 | import { LoginContainer } from '~/components/login/Container' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | export default function Kun() { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/resource/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { resourceSchema } from '~/validations/resource' 6 | import { getPatchResource } from '~/app/api/resource/route' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | 9 | export const kunGetActions = async (params: z.infer) => { 10 | const input = safeParseSchema(resourceSchema, params) 11 | if (typeof input === 'string') { 12 | return input 13 | } 14 | 15 | const nsfwEnable = await getNSFWHeader() 16 | 17 | const response = await getPatchResource(input, nsfwEnable) 18 | return response 19 | } 20 | -------------------------------------------------------------------------------- /app/search/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import { SUPPORTED_TYPE_MAP } from '~/constants/resource' 3 | import type { Metadata } from 'next' 4 | 5 | export const kunMetadata: Metadata = { 6 | title: '搜索 Galgame', 7 | description: `您可以在此处搜索所有的 Galgame 资源, 包括 ${Object.values(SUPPORTED_TYPE_MAP)} 等等`, 8 | openGraph: { 9 | title: '搜索 Galgame', 10 | description: `您可以在此处搜索所有的 Galgame 资源, 包括 ${Object.values(SUPPORTED_TYPE_MAP)} 等等`, 11 | type: 'website', 12 | images: kunMoyuMoe.images 13 | }, 14 | twitter: { 15 | card: 'summary_large_image', 16 | title: '搜索 Galgame', 17 | description: `您可以在此处搜索所有的 Galgame 资源, 包括 ${Object.values(SUPPORTED_TYPE_MAP)} 等等` 18 | }, 19 | alternates: { 20 | canonical: `${kunMoyuMoe.domain.main}/search` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { SearchPage } from '~/components/search/Container' 2 | import { kunMetadata } from './metadata' 3 | import { Suspense } from 'react' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | export default function Search() { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/settings/user/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: '用户信息设置', 6 | description: 7 | '您可以在此处任何更改您的个人信息, 头像, 签名, 用户名, 更改密码, 更改邮箱', 8 | openGraph: { 9 | title: '用户信息设置', 10 | description: 11 | '您可以在此处任何更改您的个人信息, 头像, 签名, 用户名, 更改密码, 更改邮箱', 12 | type: 'website', 13 | images: kunMoyuMoe.images 14 | }, 15 | twitter: { 16 | card: 'summary_large_image', 17 | title: '用户信息设置', 18 | description: 19 | '您可以在此处任何更改您的个人信息, 头像, 签名, 用户名, 更改密码, 更改邮箱' 20 | }, 21 | alternates: { 22 | canonical: `${kunMoyuMoe.domain.main}/settings/user` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/settings/user/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserSettings } from '~/components/settings/user/User' 2 | import { kunMetadata } from './metadata' 3 | import type { Metadata } from 'next' 4 | 5 | export const metadata: Metadata = kunMetadata 6 | 7 | export default function User() { 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /app/tag/[id]/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getTagById } from '~/app/api/tag/get' 6 | import { getPatchByTag } from '~/app/api/tag/galgame/route' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | import { getTagByIdSchema, getPatchByTagSchema } from '~/validations/tag' 9 | 10 | export const kunGetTagByIdActions = async ( 11 | params: z.infer 12 | ) => { 13 | const input = safeParseSchema(getTagByIdSchema, params) 14 | if (typeof input === 'string') { 15 | return input 16 | } 17 | 18 | const response = await getTagById(input) 19 | return response 20 | } 21 | 22 | export const kunTagGalgameActions = async ( 23 | params: z.infer 24 | ) => { 25 | const input = safeParseSchema(getPatchByTagSchema, params) 26 | if (typeof input === 'string') { 27 | return input 28 | } 29 | 30 | const nsfwEnable = await getNSFWHeader() 31 | 32 | const response = await getPatchByTag(input, nsfwEnable) 33 | return response 34 | } 35 | -------------------------------------------------------------------------------- /app/tag/[id]/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | import type { Metadata } from 'next' 3 | import type { TagDetail } from '~/types/api/tag' 4 | 5 | export const generateKunMetadataTemplate = (tag: TagDetail): Metadata => { 6 | const title = `标签 - ${tag.name}` 7 | return generateNullMetadata(title) 8 | } 9 | -------------------------------------------------------------------------------- /app/tag/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getTagSchema } from '~/validations/tag' 6 | import { getTag } from '~/app/api/tag/all/route' 7 | 8 | export const kunGetActions = async (params: z.infer) => { 9 | const input = safeParseSchema(getTagSchema, params) 10 | if (typeof input === 'string') { 11 | return input 12 | } 13 | 14 | const response = await getTag(input) 15 | return response 16 | } 17 | -------------------------------------------------------------------------------- /app/tag/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | ...generateNullMetadata(`Galgame 标签`) 6 | } 7 | -------------------------------------------------------------------------------- /app/tag/page.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '~/components/tag/Container' 2 | import { kunMetadata } from './metadata' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { Suspense } from 'react' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 3 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const response = await kunGetActions({ 14 | page: 1, 15 | limit: 100 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/user/[id]/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 5 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserProfile } from '~/app/api/user/status/info/route' 7 | 8 | const getProfileSchema = z.object({ 9 | id: z.coerce.number().min(1).max(9999999) 10 | }) 11 | 12 | export const kunGetActions = async (id: number) => { 13 | const input = safeParseSchema(getProfileSchema, { id }) 14 | if (typeof input === 'string') { 15 | return input 16 | } 17 | const payload = await verifyHeaderCookie() 18 | 19 | const user = await getUserProfile(input, payload?.uid ?? 0) 20 | return user 21 | } 22 | -------------------------------------------------------------------------------- /app/user/[id]/comment/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getUserInfoSchema } from '~/validations/user' 5 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 6 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 7 | import { getUserComment } from '~/app/api/user/profile/comment/route' 8 | 9 | export const kunGetActions = async ( 10 | params: z.infer 11 | ) => { 12 | const input = safeParseSchema(getUserInfoSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | const payload = await verifyHeaderCookie() 17 | if (!payload) { 18 | return '用户登陆失效' 19 | } 20 | 21 | const response = await getUserComment(input) 22 | return response 23 | } 24 | -------------------------------------------------------------------------------- /app/user/[id]/comment/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserComment } from '~/components/user/comment/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | import { Suspense } from 'react' 5 | 6 | export const revalidate = 3 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export default async function Kun({ params }: Props) { 13 | const { id } = await params 14 | 15 | const response = await kunGetActions({ 16 | uid: Number(id), 17 | page: 1, 18 | limit: 20 19 | }) 20 | if (typeof response === 'string') { 21 | return 22 | } 23 | 24 | return ( 25 | 26 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/user/[id]/favorite/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 4 | import { getFolders } from '~/app/api/user/profile/favorite/folder/get' 5 | 6 | export const kunGetActions = async (uid: number) => { 7 | const payload = await verifyHeaderCookie() 8 | if (!payload) { 9 | return '用户登陆失效' 10 | } 11 | 12 | const response = await getFolders({}, uid, payload.uid) 13 | return { folders: response, currentUserUid: payload.uid } 14 | } 15 | -------------------------------------------------------------------------------- /app/user/[id]/favorite/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserFavorite } from '~/components/user/favorite/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | 5 | export const revalidate = 3 6 | 7 | interface Props { 8 | params: Promise<{ id: string }> 9 | } 10 | 11 | export default async function Kun({ params }: Props) { 12 | const { id } = await params 13 | 14 | const response = await kunGetActions(Number(id)) 15 | if (typeof response === 'string') { 16 | return 17 | } 18 | 19 | return ( 20 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/user/[id]/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | import type { Metadata } from 'next' 3 | import type { UserInfo } from '~/types/api/user' 4 | 5 | export const generateKunMetadataTemplate = (user: UserInfo): Metadata => { 6 | return generateNullMetadata(`${user.name} 的主页`) 7 | } 8 | -------------------------------------------------------------------------------- /app/user/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function Kun({ 4 | params 5 | }: { 6 | params: Promise<{ id: string }> 7 | }) { 8 | const { id } = await params 9 | redirect(`/user/${id}/resource`) 10 | } 11 | -------------------------------------------------------------------------------- /app/user/[id]/resource/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getUserInfoSchema } from '~/validations/user' 5 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 6 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 7 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 8 | import { getUserPatchResource } from '~/app/api/user/profile/resource/route' 9 | 10 | export const kunGetActions = async ( 11 | params: z.infer 12 | ) => { 13 | const input = safeParseSchema(getUserInfoSchema, params) 14 | if (typeof input === 'string') { 15 | return input 16 | } 17 | const payload = await verifyHeaderCookie() 18 | if (!payload) { 19 | return '用户登陆失效' 20 | } 21 | 22 | const nsfwEnable = await getNSFWHeader() 23 | 24 | const response = await getUserPatchResource(input, nsfwEnable) 25 | return response 26 | } 27 | -------------------------------------------------------------------------------- /app/user/[id]/resource/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserResource } from '~/components/user/resource/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | import { Suspense } from 'react' 5 | 6 | export const revalidate = 3 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export default async function Kun({ params }: Props) { 13 | const { id } = await params 14 | 15 | const response = await kunGetActions({ 16 | uid: Number(id), 17 | page: 1, 18 | limit: 20 19 | }) 20 | if (typeof response === 'string') { 21 | return 22 | } 23 | 24 | return ( 25 | 26 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/user/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function Kun() { 4 | redirect('/') 5 | } 6 | -------------------------------------------------------------------------------- /components/admin/Navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | NavbarContent, 5 | NavbarItem, 6 | Navbar as NextUINavbar 7 | } from '@nextui-org/react' 8 | 9 | export const Navbar = () => { 10 | return ( 11 | 12 | 13 | 哦哈呦~ 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/admin/email/Container.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { EmailTemplate } from './EmailTemplate' 4 | 5 | export const EmailSetting = () => { 6 | return ( 7 |
8 |
9 |

邮件群发

10 |
11 | 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/admin/setting/Container.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from '@nextui-org/divider' 2 | import { RedirectSetting } from './RedirectSetting' 3 | import { DisableRegisterSetting } from './DisableRegisterSetting' 4 | import type { AdminRedirectConfig } from '~/types/api/admin' 5 | 6 | interface Props { 7 | setting: AdminRedirectConfig 8 | disableRegister: boolean 9 | } 10 | 11 | export const AdminSetting = ({ setting, disableRegister }: Props) => { 12 | return ( 13 |
14 |
15 |

网站设置

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/admin/stats/KunStats.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Divider } from '@nextui-org/react' 3 | // import { KunAdminLineChart } from './KunAdminLineChart' 4 | import { KunAdminStatistic } from './KunAdminStatistic' 5 | 6 | export const KunStats: FC = () => { 7 | return ( 8 |
9 | 10 | 11 | {/* */} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /components/admin/stats/StatsCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardBody } from '@nextui-org/react' 4 | import type { FC } from 'react' 5 | 6 | export const StatsCard: FC<{ title: string; value: number }> = ({ 7 | title, 8 | value 9 | }) => ( 10 | 11 | 12 |

13 | {title} 14 |

15 |

16 | {value.toLocaleString()} 17 |

18 |
19 |
20 | ) 21 | -------------------------------------------------------------------------------- /components/auth/email-notice/Container.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { kunMoyuMoe } from '~/config/moyu-moe' 4 | import { Suspense } from 'react' 5 | import { KunEmailNoticeCard } from './KunEmailNoticeCard' 6 | 7 | export const KunContainer = () => { 8 | return ( 9 |
10 |
11 |
12 |

邮件通知退订

13 |

14 | 如果您要退订, 请点击下面的按钮, 您将不再收到任何关于{' '} 15 | {kunMoyuMoe.titleShort} 的邮件通知 16 |

17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/comment/_sort.d.ts: -------------------------------------------------------------------------------- 1 | export type SortOption = 'created' | 'like' 2 | export type SortDirection = 'asc' | 'desc' 3 | -------------------------------------------------------------------------------- /components/doc/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@nextui-org/react' 2 | import { Search } from 'lucide-react' 3 | import { KunHeader } from '../kun/Header' 4 | 5 | // interface HeaderProps { 6 | // onSearch: (value: string) => void 7 | // } 8 | 9 | export const KunAboutHeader = () => { 10 | return ( 11 |
12 | 16 | 17 | {/* } 24 | onChange={(e) => onSearch(e.target.value)} 25 | /> */} 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/doc/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { KunTreeNode } from '~/lib/mdx/types' 4 | import { TreeItem } from './SideTreeItem' 5 | 6 | interface Props { 7 | tree: KunTreeNode 8 | } 9 | 10 | export const SidebarContent = ({ tree }: Props) => { 11 | return ( 12 |
13 | {tree.type === 'directory' && 14 | tree.children?.map((child, index) => ( 15 | 16 | ))} 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/edit/VNDB.d.ts: -------------------------------------------------------------------------------- 1 | export interface Title { 2 | title: string 3 | lang: 'ja' | 'zh-Hans' | 'en' 4 | } 5 | 6 | export interface VNDBData { 7 | id: number 8 | title: string 9 | titles: Title[] 10 | aliases: string[] 11 | languages: string[] 12 | platforms: string[] 13 | } 14 | 15 | export interface VNDB { 16 | title: string 17 | titles: Title[] 18 | aliases: string[] 19 | released: string 20 | } 21 | 22 | export interface VNDBResponse { 23 | more: boolean 24 | results: VNDB[] 25 | } 26 | -------------------------------------------------------------------------------- /components/edit/components/BatchTag.tsx: -------------------------------------------------------------------------------- 1 | import { Textarea } from '@nextui-org/react' 2 | 3 | interface Props { 4 | initialTag: string[] 5 | saveTag: (tag: string[]) => void 6 | errors?: string 7 | } 8 | 9 | export const BatchTag = ({ initialTag, saveTag, errors }: Props) => { 10 | return ( 11 |
12 |

游戏标签 (可选)

13 | {errors &&

{errors}

} 14 |