├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .github ├── renovate.json └── workflows │ └── lint-check.yml ├── .gitignore ├── .idx └── dev.nix ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── app ├── about │ ├── [...slug] │ │ ├── metadata.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── metadata.ts │ └── page.tsx ├── actions.ts ├── admin │ ├── comment │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── creator │ │ ├── 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 │ ├── 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 │ │ ├── galgame │ │ │ └── route.ts │ │ ├── log │ │ │ └── route.ts │ │ ├── resource │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── setting │ │ │ ├── comment │ │ │ │ ├── getCommentVerifyStatus.ts │ │ │ │ └── route.ts │ │ │ ├── creator │ │ │ │ ├── getEnableOnlyCreatorCreateStatus.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 │ │ ├── login │ │ │ └── route.ts │ │ ├── register │ │ │ └── route.ts │ │ └── send-register-code │ │ │ └── route.ts │ ├── comment │ │ └── route.ts │ ├── company │ │ ├── all │ │ │ └── route.ts │ │ ├── galgame │ │ │ └── route.ts │ │ ├── route.ts │ │ ├── search │ │ │ └── route.ts │ │ └── upload-logo │ │ │ └── route.ts │ ├── edit │ │ ├── _helpers.ts │ │ ├── create.ts │ │ ├── duplicate │ │ │ └── route.ts │ │ ├── route.ts │ │ └── update.ts │ ├── forgot │ │ ├── one │ │ │ └── route.ts │ │ └── two │ │ │ └── route.ts │ ├── galgame │ │ └── route.ts │ ├── hikari │ │ ├── route.ts │ │ └── type.d.ts │ ├── home │ │ └── 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 │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── contributor │ │ │ └── route.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── history │ │ │ └── route.ts │ │ ├── introduction │ │ │ ├── company │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── tag │ │ │ │ └── route.ts │ │ ├── like │ │ │ └── route.ts │ │ ├── pr │ │ │ ├── decline │ │ │ │ └── route.ts │ │ │ ├── merge │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── resource │ │ │ ├── _helper.ts │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── download │ │ │ │ └── route.ts │ │ │ ├── get.ts │ │ │ ├── like │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ │ ├── route.ts │ │ └── walkthrough │ │ │ ├── create.ts │ │ │ ├── delete.ts │ │ │ ├── get.ts │ │ │ ├── route.ts │ │ │ └── update.ts │ ├── release │ │ └── route.ts │ ├── resource │ │ └── route.ts │ ├── search │ │ └── route.ts │ ├── tag │ │ ├── all │ │ │ └── route.ts │ │ ├── galgame │ │ │ └── route.ts │ │ ├── route.ts │ │ └── search │ │ │ └── route.ts │ ├── upload │ │ ├── calculateFileStreamHash.ts │ │ ├── resource │ │ │ └── route.ts │ │ └── utils.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 │ │ │ ├── contribute │ │ │ │ └── route.ts │ │ │ ├── favorite │ │ │ │ └── route.ts │ │ │ ├── floating │ │ │ │ └── route.ts │ │ │ ├── galgame │ │ │ │ └── route.ts │ │ │ └── resource │ │ │ │ └── route.ts │ │ ├── setting │ │ │ ├── _upload.ts │ │ │ ├── avatar │ │ │ │ └── route.ts │ │ │ ├── bio │ │ │ │ └── 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 │ │ ├── constants.ts │ │ ├── createMentionMessage.ts │ │ ├── generateRandomCode.ts │ │ ├── getNSFWHeader.ts │ │ ├── getRemoteIp.ts │ │ ├── jwt.ts │ │ ├── markdownToHtml.ts │ │ ├── message.ts │ │ ├── parseQuery.ts │ │ ├── sendVerificationCodeEmail.ts │ │ ├── uploadPatchBanner.ts │ │ ├── verifyKunCaptcha.ts │ │ └── verifyVerificationCode.ts ├── apply │ ├── actions.ts │ ├── metadata.ts │ ├── page.tsx │ ├── pending │ │ ├── metadata.ts │ │ └── page.tsx │ └── success │ │ ├── metadata.ts │ │ └── page.tsx ├── auth │ └── forgot │ │ ├── metadata.ts │ │ └── page.tsx ├── check-hash │ ├── metadata.ts │ └── page.tsx ├── comment │ ├── actions.ts │ ├── metadata.ts │ └── page.tsx ├── company │ ├── [id] │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── actions.ts │ ├── 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 │ ├── 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 │ ├── patch-resource-create │ │ ├── metadata.ts │ │ └── page.tsx │ ├── patch-resource-update │ │ ├── metadata.ts │ │ └── page.tsx │ └── system │ │ ├── metadata.ts │ │ └── page.tsx ├── metadata.ts ├── page.tsx ├── patch │ └── [id] │ │ ├── actions.ts │ │ ├── comment │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ │ ├── history │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ │ ├── introduction │ │ ├── metadata.ts │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── metadata.ts │ │ ├── page.tsx │ │ ├── pr │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ │ ├── resource │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ │ └── walkthrough │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx ├── providers.tsx ├── ranking │ ├── actions.ts │ ├── page.tsx │ ├── patch │ │ ├── metadata.ts │ │ └── page.tsx │ └── user │ │ ├── metadata.ts │ │ └── page.tsx ├── register │ ├── metadata.ts │ └── page.tsx ├── release │ ├── actions.ts │ ├── 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 │ │ ├── metadata.ts │ │ └── page.tsx │ ├── contribute │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── favorite │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── galgame │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ ├── layout.tsx │ ├── metadata.ts │ ├── page.tsx │ └── resource │ │ ├── actions.ts │ │ ├── metadata.ts │ │ └── page.tsx │ └── page.tsx ├── components ├── about │ ├── BlogHeader.tsx │ ├── Card.tsx │ ├── Header.tsx │ ├── Navigation.tsx │ ├── SideTreeItem.tsx │ ├── Sidebar.tsx │ ├── SidebarContent.tsx │ └── TableOfContents.tsx ├── admin │ ├── Navbar.tsx │ ├── Sidebar.tsx │ ├── SidebarContent.tsx │ ├── comment │ │ ├── Card.tsx │ │ ├── CommentEdit.tsx │ │ └── Container.tsx │ ├── creator │ │ ├── ActionButton.tsx │ │ ├── Container.tsx │ │ └── RenderCell.tsx │ ├── galgame │ │ ├── Container.tsx │ │ └── RenderCell.tsx │ ├── log │ │ ├── Card.tsx │ │ └── Container.tsx │ ├── resource │ │ ├── Container.tsx │ │ ├── RenderCell.tsx │ │ └── ResourceEdit.tsx │ ├── setting │ │ ├── Container.tsx │ │ ├── DisableRegisterSetting.tsx │ │ ├── EnableCommentVerify.tsx │ │ └── EnableOnlyCreatorCreateGalgame.tsx │ ├── stats │ │ ├── KunAdminStatistic.tsx │ │ ├── KunAdminSum.tsx │ │ ├── KunStats.tsx │ │ └── StatsCard.tsx │ └── user │ │ ├── Container.tsx │ │ ├── RenderCell.tsx │ │ ├── UserDelete.tsx │ │ └── UserEdit.tsx ├── apply │ ├── Container.tsx │ └── Success.tsx ├── check-hash │ ├── CheckHash.tsx │ └── Container.tsx ├── comment │ ├── CommentCard.tsx │ ├── Container.tsx │ ├── FilterBar.tsx │ └── _sort.d.ts ├── company │ ├── Card.tsx │ ├── CompanyHeader.tsx │ ├── CompanyList.tsx │ ├── Container.tsx │ ├── SearchCompanies.tsx │ ├── detail │ │ └── Container.tsx │ └── form │ │ ├── ArrayAdder.tsx │ │ ├── CompanyFormModal.tsx │ │ └── LogoImage.tsx ├── edit │ ├── VNDB.d.ts │ ├── components │ │ └── 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 ├── forgot │ ├── Forgot.tsx │ ├── StepOne.tsx │ └── StepTwo.tsx ├── friend-link │ └── KunFriendLink.tsx ├── galgame │ ├── Card.tsx │ ├── Container.tsx │ ├── FilterBar.tsx │ └── _sort.d.ts ├── home │ ├── Container.tsx │ ├── Hero.tsx │ ├── PatchCard.tsx │ └── carousel │ │ ├── DesktopCard.tsx │ │ ├── KunCarousel.tsx │ │ ├── KunMobileCard.tsx │ │ ├── KunNavigationMenu.tsx │ │ └── mdx.ts ├── kun │ ├── BackToTop.tsx │ ├── CardStats.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── Loading.tsx │ ├── MasonryGrid.tsx │ ├── NSFWIndicator.tsx │ ├── NavigationBreadcrumb.tsx │ ├── Null.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 │ ├── floating-card │ │ ├── KunAvatar.tsx │ │ ├── KunUser.tsx │ │ ├── KunUserCard.tsx │ │ └── KunUserStatCard.tsx │ ├── icons │ │ ├── GitHub.tsx │ │ ├── Markdown.tsx │ │ ├── Microsoft.tsx │ │ └── Telegram.tsx │ ├── image-viewer │ │ ├── AutoImageViewer.tsx │ │ └── ImageViewer.tsx │ ├── milkdown │ │ ├── Editor.tsx │ │ ├── EditorProvider.tsx │ │ ├── PatchEditor.tsx │ │ └── plugins │ │ │ ├── Menu.tsx │ │ │ ├── MenuButton.tsx │ │ │ ├── _buttonList.ts │ │ │ ├── emoji │ │ │ ├── EmojiPicker.tsx │ │ │ └── _isoEmoji.ts │ │ │ ├── mention │ │ │ └── MentionsListDropdown.tsx │ │ │ ├── placeholder │ │ │ └── placeholderPlugin.ts │ │ │ ├── stop-link │ │ │ └── stopLinkPlugin.ts │ │ │ └── uploader.ts │ ├── top-bar │ │ ├── Brand.tsx │ │ ├── KunMobileMenu.tsx │ │ ├── NSFWSwitcher.tsx │ │ ├── Search.tsx │ │ ├── ThemeSwitcher.tsx │ │ ├── TopBar.tsx │ │ ├── User.tsx │ │ ├── UserDropdown.tsx │ │ ├── UserMessageBell.tsx │ │ └── user │ │ │ ├── CheckIn.tsx │ │ │ └── Logout.tsx │ ├── utils │ │ └── loli.ts │ └── verification-code │ │ └── Code.tsx ├── login │ └── Login.tsx ├── message │ ├── Container.tsx │ ├── MessageCard.tsx │ └── MessageNav.tsx ├── patch │ ├── Container.tsx │ ├── Contributor.tsx │ ├── DiffContent.tsx │ ├── _diff.scss │ ├── comment │ │ ├── CommentContent.tsx │ │ ├── CommentDropdown.tsx │ │ ├── CommentLike.tsx │ │ ├── Comments.tsx │ │ ├── PublishComment.tsx │ │ └── _scrollIntoComment.ts │ ├── header │ │ ├── Actions.tsx │ │ ├── BackgroundImage.tsx │ │ ├── Container.tsx │ │ ├── EditBanner.tsx │ │ ├── Info.tsx │ │ ├── Tabs.tsx │ │ └── Tags.tsx │ ├── history │ │ ├── Card.tsx │ │ └── History.tsx │ ├── introduction │ │ ├── Company.tsx │ │ ├── Container.tsx │ │ ├── Info.tsx │ │ ├── PatchCompanySelector.tsx │ │ ├── PatchTagSelector.tsx │ │ └── Tag.tsx │ ├── pr │ │ └── PullRequest.tsx │ ├── resource │ │ ├── DownloadCard.tsx │ │ ├── Resource.tsx │ │ ├── ResourceDownload.tsx │ │ ├── ResourceInfo.tsx │ │ ├── ResourceLike.tsx │ │ ├── edit │ │ │ └── EditResourceDialog.tsx │ │ ├── publish │ │ │ ├── PublishResource.tsx │ │ │ ├── ResourceDetailsForm.tsx │ │ │ ├── ResourceLinksInput.tsx │ │ │ └── ResourceTypeSelect.tsx │ │ ├── share.d.ts │ │ └── upload │ │ │ ├── FileDropZone.tsx │ │ │ ├── FileUploadCard.tsx │ │ │ ├── FileUploadContainer.tsx │ │ │ └── utils.ts │ └── walkthrough │ │ ├── Card.tsx │ │ ├── Container.tsx │ │ ├── PublishWalkthrough.tsx │ │ └── WalkthroughDropdown.tsx ├── ranking │ ├── Container.tsx │ ├── patch │ │ └── PatchList.tsx │ └── user │ │ ├── UserCard.tsx │ │ ├── UserList.tsx │ │ └── UserStatsItem.tsx ├── register │ └── Register.tsx ├── release │ ├── Container.tsx │ ├── MonthNavigation.tsx │ └── ReleaseCard.tsx ├── resource │ ├── Container.tsx │ ├── FilterBar.tsx │ ├── ResourceCard.tsx │ └── _sort.d.ts ├── search │ ├── Container.tsx │ └── SearchHistory.tsx ├── settings │ ├── Nav.tsx │ └── user │ │ ├── Avatar.tsx │ │ ├── AvatarCrop.tsx │ │ ├── Bio.tsx │ │ ├── Container.tsx │ │ ├── Email.tsx │ │ ├── MessageSettings.tsx │ │ ├── Password.tsx │ │ ├── Reset.tsx │ │ └── Username.tsx ├── tag │ ├── Card.tsx │ ├── Container.tsx │ ├── CreateTagModal.tsx │ ├── SearchTag.tsx │ ├── TagHeader.tsx │ ├── TagList.tsx │ └── detail │ │ ├── Container.tsx │ │ └── EditTagModal.tsx └── user │ ├── Activity.tsx │ ├── Profile.tsx │ ├── SelfButton.tsx │ ├── Stats.tsx │ ├── comment │ ├── Card.tsx │ └── Container.tsx │ ├── contribute │ ├── Card.tsx │ └── Container.tsx │ ├── favorite │ └── Container.tsx │ ├── follow │ ├── Follow.tsx │ ├── Stats.tsx │ └── UserList.tsx │ ├── galgame │ ├── Card.tsx │ └── Container.tsx │ └── resource │ ├── Card.tsx │ └── Container.tsx ├── config ├── config.d.ts ├── email-whitelist.ts ├── friend.json ├── friend.ts ├── moyu-moe.ts ├── redis.ts ├── search.ts ├── upload.ts └── user.ts ├── constants ├── about.ts ├── admin.ts ├── api │ └── select.ts ├── captcha.ts ├── company.ts ├── galgame.ts ├── history.ts ├── home-typed-js.ts ├── message.ts ├── ranking.tsx ├── 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 ├── instrumentation.ts ├── lib ├── mdx │ ├── CustomMDX.tsx │ ├── directoryTree.ts │ ├── element │ │ ├── KunCode.tsx │ │ ├── KunLink.tsx │ │ ├── KunTable.tsx │ │ └── kunHeading.ts │ ├── getPosts.ts │ └── types.d.ts ├── redis.ts ├── reteLimiter.ts ├── s3.ts └── s3 │ ├── client.ts │ ├── deleteFileFromS3.ts │ ├── uploadImageToS3.ts │ ├── uploadLargeFileToS3.ts │ └── uploadSmallFileToS3.ts ├── middleware ├── _verifyHeaderCookie.ts ├── auth.ts └── middleware.ts ├── migration ├── fetchVNDBInfo.mjs ├── migrateAlias.mjs ├── saveAlias.mjs ├── syncResourceCategories.mjs ├── updatePatchResourceUpdateTime.mjs ├── updateReleased.mjs ├── updateResourceUpdateTime.mjs └── userDailyUploadSize.mjs ├── motion ├── bell.ts ├── card.ts └── sooner.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── posts ├── dev │ └── documentation.mdx ├── galgame │ └── resource.mdx ├── kun │ ├── moe.mdx │ └── ren.mdx └── notice │ ├── about.mdx │ ├── cfmsc.mdx │ ├── creator.mdx │ ├── feedback.mdx │ ├── galgame-tutorial.mdx │ ├── moemoepoint.mdx │ ├── nsfw.mdx │ ├── open-source.mdx │ ├── paradigm.mdx │ ├── patch-tutorial.mdx │ ├── privacy.mdx │ ├── rule.mdx │ └── update.mdx ├── prisma ├── index.ts └── schema.prisma ├── public ├── apple-touch-icon.avif ├── edit │ ├── 1.avif │ └── 2.avif ├── favicon.ico ├── favicon.webp ├── kungalgame.avif ├── placeholder.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 │ │ ├── feedback │ │ └── banner.avif │ │ ├── galgame-tutorial │ │ └── banner.avif │ │ ├── moemoepoint │ │ └── banner.avif │ │ ├── nsfw │ │ └── 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 │ │ └── update │ │ └── banner.avif ├── robots.txt └── sooner │ ├── あーちゃん.webp │ ├── こじかひわ.webp │ ├── 琥珀.webp │ └── 雪々.webp ├── scripts ├── deployBuild.ts ├── deployInstall.ts ├── dynamic-routes │ ├── getKunDynamicBlog.ts │ └── getKunDynamicRoutes.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 │ ├── company.d.ts │ ├── galgame.d.ts │ ├── home.d.ts │ ├── message.d.ts │ ├── patch.d.ts │ ├── ranking.d.ts │ ├── release.d.ts │ ├── resource.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 ├── company.ts ├── dotenv-check.ts ├── edit.ts ├── forgot.ts ├── galgame.ts ├── message.ts ├── patch.ts ├── release.ts ├── resource.ts ├── search.ts ├── tag.ts ├── user.ts └── walkthrough.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:20 2 | 3 | # Install pnpm 4 | RUN su node -c "npm i -g pnpm" 5 | 6 | # Install additional OS packages 7 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 8 | && apt-get -y install --no-install-recommends postgresql-client redis-tools 9 | 10 | # Clean up 11 | RUN apt-get autoremove -y \ 12 | && apt-get clean -y \ 13 | && rm -rf /var/lib/apt/lists/* 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kungalgame Patch Development Environment", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "app", 5 | "workspaceFolder": "/workspace", 6 | "customizations": { 7 | "vscode": { 8 | "settings": { 9 | "eslint.alwaysShowStatus": true, 10 | "eslint.codeActionsOnSave.mode": "all", 11 | "eslint.enable": true, 12 | "eslint.format.enable": true, 13 | "editor.formatOnSave": true, 14 | "prettier.requireConfig": true 15 | } 16 | } 17 | }, 18 | "features": { 19 | "ghcr.io/devcontainers/features/node:1": { 20 | "version": "22" 21 | }, 22 | "ghcr.io/devcontainers/features/git:1": {}, 23 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 24 | }, 25 | "forwardPorts": [3000, 5432, 6379], 26 | "postCreateCommand": "pnpm install && pnpm prisma:push" 27 | } 28 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ..:/workspace:cached 10 | command: sleep infinity 11 | network_mode: service:db 12 | environment: 13 | - NODE_ENV=development 14 | - KUN_DATABASE_URL=postgresql://postgres:kunloveren@localhost:5432/kungalgame_patch?schema=public 15 | - REDIS_HOST=localhost 16 | - REDIS_PORT=6379 17 | 18 | db: 19 | image: postgres:latest 20 | restart: unless-stopped 21 | volumes: 22 | - postgres-data:/var/lib/postgresql/data 23 | environment: 24 | POSTGRES_PASSWORD: kunloveren 25 | POSTGRES_USER: postgres 26 | POSTGRES_DB: kungalgame_patch 27 | 28 | redis: 29 | image: redis:latest 30 | restart: unless-stopped 31 | volumes: 32 | - redis-data:/data 33 | network_mode: service:db 34 | 35 | volumes: 36 | postgres-data: 37 | redis-data: 38 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageRules": [ 3 | { 4 | "managers": ["npm"], 5 | "packageNames": ["pnpm"], 6 | "enabled": true, 7 | "rangeStrategy": "bump" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.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 | 41 | # uploads 42 | /uploads 43 | 44 | # backup 45 | /backup 46 | 47 | # sitemap 48 | /public/**/*.xml 49 | 50 | # IDX 51 | .idx/.data 52 | 53 | # migration cache 54 | migration/*.json 55 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* 2 | -------------------------------------------------------------------------------- /.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/about/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { KunSidebar } from '~/components/about/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/about/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/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getNSFWHeader } from '~/utils/actions/getNSFWHeader' 4 | import { getHomeData } from '~/app/api/home/route' 5 | 6 | export const kunGetActions = async () => { 7 | const nsfwEnable = await getNSFWHeader() 8 | const response = await getHomeData(nsfwEnable) 9 | return response 10 | } 11 | -------------------------------------------------------------------------------- /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 | description: `管理 ${kunMoyuMoe.titleShort} 所有的评论, 查询所有评论, 删除评论, 更改评论内容, 查看评论列表 等等`, 7 | openGraph: { 8 | title: `评论管理 - ${kunMoyuMoe.titleShort}`, 9 | description: `管理 ${kunMoyuMoe.titleShort} 所有的评论, 查询所有评论, 删除评论, 更改评论内容, 查看评论列表 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `评论管理 - ${kunMoyuMoe.titleShort}`, 16 | description: `管理 ${kunMoyuMoe.titleShort} 所有的评论, 查询所有评论, 删除评论, 更改评论内容, 查看评论列表 等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return ( 21 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /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 | description: `管理 ${kunMoyuMoe.titleShort} 所有的创作者, 同意创作者请求, 拒绝创作者请求, 查看创作者列表 等等`, 7 | openGraph: { 8 | title: `创作者管理 - ${kunMoyuMoe.titleShort}`, 9 | description: `管理 ${kunMoyuMoe.titleShort} 所有的创作者, 同意创作者请求, 拒绝创作者请求, 查看创作者列表 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `创作者管理 - ${kunMoyuMoe.titleShort}`, 16 | description: `管理 ${kunMoyuMoe.titleShort} 所有的创作者, 同意创作者请求, 拒绝创作者请求, 查看创作者列表 等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 = 5 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/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 | 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 getGalgame(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /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 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame, 包括创建新 Galgame, 删除 Galgame, 封禁 Galgame, 查询 Galgame, 更改 Galgame 介绍, 标签, 会社 等等`, 7 | openGraph: { 8 | title: `Galgame 管理 - ${kunMoyuMoe.titleShort}`, 9 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame, 包括创建新 Galgame, 删除 Galgame, 封禁 Galgame, 查询 Galgame, 更改 Galgame 介绍, 标签, 会社 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `Galgame 管理 - ${kunMoyuMoe.titleShort}`, 16 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame, 包括创建新 Galgame, 删除 Galgame, 封禁 Galgame, 查询 Galgame, 更改 Galgame 介绍, 标签, 会社 等等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return ( 21 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /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 AdminLayout({ 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 | description: `${kunMoyuMoe.titleShort} 管理系统所有的操作记录, 更改用户权限, 封禁用户, 删除 Galgame 补丁资源, 删除评论, 更改 Galgame 介绍, 标签, 别名等`, 7 | openGraph: { 8 | title: `管理日志 - ${kunMoyuMoe.titleShort}`, 9 | description: `${kunMoyuMoe.titleShort} 管理系统所有的操作记录, 更改用户权限, 封禁用户, 删除 Galgame 补丁资源, 删除评论, 更改 Galgame 介绍, 标签, 别名等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `管理日志 - ${kunMoyuMoe.titleShort}`, 16 | description: `${kunMoyuMoe.titleShort} 管理系统所有的操作记录, 更改用户权限, 封禁用户, 删除 Galgame 补丁资源, 删除评论, 更改 Galgame 介绍, 标签, 别名等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /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 管理, 补丁资源管理, 用户管理, 创作者管理, 评论管理, 系统设置, 管理日志 等等`, 7 | openGraph: { 8 | title: '管理系统', 9 | description: `为 ${kunMoyuMoe.titleShort} 定制的管理系统, 可以查看 网站概览 用户总数量, 补丁总数量, 今日评论数, 日活跃用户, 包括 Galgame 管理, 补丁资源管理, 用户管理, 创作者管理, 评论管理, 系统设置, 管理日志 等等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '管理系统', 16 | description: `为 ${kunMoyuMoe.titleShort} 定制的管理系统, 可以查看 网站概览 用户总数量, 补丁总数量, 今日评论数, 日活跃用户, 包括 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/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 | 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 getPatchResource(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /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: `补丁管理 - ${kunMoyuMoe.titleShort}`, 6 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame 补丁下载资源, 创建资源, 更改资源类型, 语言, 平台, 删除 Galgame 补丁资源 等`, 7 | openGraph: { 8 | title: `补丁管理 - ${kunMoyuMoe.titleShort}`, 9 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame 补丁下载资源, 创建资源, 更改资源类型, 语言, 平台, 删除 Galgame 补丁资源 等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `补丁管理 - ${kunMoyuMoe.titleShort}`, 16 | description: `管理 ${kunMoyuMoe.titleShort} 所有的 Galgame 补丁下载资源, 创建资源, 更改资源类型, 语言, 平台, 删除 Galgame 补丁资源 等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return ( 21 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/admin/setting/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { getDisableRegisterStatus } from '~/app/api/admin/setting/register/route' 4 | import { getCommentVerifyStatus } from '~/app/api/admin/setting/comment/getCommentVerifyStatus' 5 | import { getEnableOnlyCreatorCreateStatus } from '~/app/api/admin/setting/creator/getEnableOnlyCreatorCreateStatus' 6 | 7 | export const kunGetDisableRegisterStatusActions = async () => { 8 | const response = await getDisableRegisterStatus() 9 | return response 10 | } 11 | 12 | export const kunGetCommentVerifyStatusActions = async () => { 13 | const response = await getCommentVerifyStatus() 14 | return response 15 | } 16 | 17 | export const kunGetEnableOnlyCreatorCreateStatus = async () => { 18 | const response = await getEnableOnlyCreatorCreateStatus() 19 | return response 20 | } 21 | -------------------------------------------------------------------------------- /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 | description: `${kunMoyuMoe.titleShort} 的数据库备份, 鉴权设置, 认证设置, 网站整体风格设置, 启用维护模式 等`, 7 | openGraph: { 8 | title: `系统设置 - ${kunMoyuMoe.titleShort}`, 9 | description: `${kunMoyuMoe.titleShort} 的数据库备份, 鉴权设置, 认证设置, 网站整体风格设置, 启用维护模式 等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `系统设置 - ${kunMoyuMoe.titleShort}`, 16 | description: `${kunMoyuMoe.titleShort} 的数据库备份, 鉴权设置, 认证设置, 网站整体风格设置, 启用维护模式 等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin/setting` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/setting/page.tsx: -------------------------------------------------------------------------------- 1 | import { AdminSetting } from '~/components/admin/setting/Container' 2 | import { kunMetadata } from './metadata' 3 | import { 4 | kunGetDisableRegisterStatusActions, 5 | kunGetCommentVerifyStatusActions, 6 | kunGetEnableOnlyCreatorCreateStatus 7 | } from './actions' 8 | import type { Metadata } from 'next' 9 | 10 | export const revalidate = 5 11 | 12 | export const metadata: Metadata = kunMetadata 13 | 14 | export default async function Kun() { 15 | const { disableRegister } = await kunGetDisableRegisterStatusActions() 16 | const { enableCommentVerify } = await kunGetCommentVerifyStatusActions() 17 | const { enableOnlyCreatorCreate } = 18 | await kunGetEnableOnlyCreatorCreateStatus() 19 | 20 | return ( 21 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /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 | description: `管理 ${kunMoyuMoe.titleShort} 的所有用户, 更改用户权限, 设置用户为管理员, 设置用户为创作者, 封禁用户 等`, 7 | openGraph: { 8 | title: `用户管理 - ${kunMoyuMoe.titleShort}`, 9 | description: `管理 ${kunMoyuMoe.titleShort} 的所有用户, 更改用户权限, 设置用户为管理员, 设置用户为创作者, 封禁用户 等`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: `用户管理 - ${kunMoyuMoe.titleShort}`, 16 | description: `管理 ${kunMoyuMoe.titleShort} 的所有用户, 更改用户权限, 设置用户为管理员, 设置用户为创作者, 封禁用户 等` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/admin/user` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 30 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /app/api/admin/setting/comment/getCommentVerifyStatus.ts: -------------------------------------------------------------------------------- 1 | import { getKv } from '~/lib/redis' 2 | import { KUN_PATCH_ENABLE_COMMENT_VERIFY_KEY } from '~/config/redis' 3 | 4 | export const getCommentVerifyStatus = async () => { 5 | const isEnableCommentVerify = await getKv(KUN_PATCH_ENABLE_COMMENT_VERIFY_KEY) 6 | return { 7 | enableCommentVerify: !!isEnableCommentVerify 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/api/admin/setting/creator/getEnableOnlyCreatorCreateStatus.ts: -------------------------------------------------------------------------------- 1 | import { getKv } from '~/lib/redis' 2 | import { KUN_PATCH_ENABLE_ONLY_CREATOR_CREATE_KEY } from '~/config/redis' 3 | 4 | export const getEnableOnlyCreatorCreateStatus = async () => { 5 | const isEnableOnlyCreatorCreate = await getKv( 6 | KUN_PATCH_ENABLE_ONLY_CREATOR_CREATE_KEY 7 | ) 8 | return { 9 | enableOnlyCreatorCreate: !!isEnableOnlyCreatorCreate 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /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 [userCount, galgameCount, patchResourceCount, patchCommentCount] = 7 | await Promise.all([ 8 | prisma.user.count(), 9 | prisma.patch.count(), 10 | prisma.patch_resource.count(), 11 | prisma.patch_comment.count() 12 | ]) 13 | 14 | return { 15 | userCount, 16 | galgameCount, 17 | patchResourceCount, 18 | patchCommentCount 19 | } 20 | } 21 | 22 | export const GET = async () => { 23 | const data = await getSumData() 24 | return NextResponse.json(data) 25 | } 26 | -------------------------------------------------------------------------------- /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 { verifyKunCaptcha } from '~/app/api/utils/verifyKunCaptcha' 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 verifyKunCaptcha(input.sessionId, input.selectedIds) 19 | return NextResponse.json(captcha) 20 | } 21 | -------------------------------------------------------------------------------- /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/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.findMany({ 7 | where: { recipient_id: uid, status: 0 }, 8 | select: { type: true } 9 | }) 10 | return [...new Set(unread.map((message) => message.type))] 11 | } 12 | 13 | export const GET = async (req: NextRequest) => { 14 | const payload = await verifyHeaderCookie(req) 15 | if (!payload) { 16 | return NextResponse.json('用户未登录') 17 | } 18 | 19 | const response = await getMessage(payload.uid) 20 | return NextResponse.json(response) 21 | } 22 | -------------------------------------------------------------------------------- /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 | ) => { 9 | const { commentId, content } = input 10 | 11 | await prisma.patch_comment.update({ 12 | where: { id: commentId, user_id: uid }, 13 | data: { 14 | content, 15 | edit: Date.now().toString() 16 | }, 17 | include: { 18 | user: true, 19 | like_by: { 20 | include: { 21 | user: true 22 | } 23 | } 24 | } 25 | }) 26 | return {} 27 | } 28 | -------------------------------------------------------------------------------- /app/api/patch/walkthrough/delete.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | 4 | const walkthroughIdSchema = z.object({ 5 | walkthroughId: z.coerce.number().min(1).max(9999999) 6 | }) 7 | 8 | export const deleteWalkthrough = async ( 9 | input: z.infer, 10 | uid: number 11 | ) => { 12 | const patchWalkthrough = await prisma.patch_walkthrough.findUnique({ 13 | where: { 14 | id: input.walkthroughId, 15 | user_id: uid 16 | } 17 | }) 18 | if (!patchWalkthrough) { 19 | return '未找到对应的攻略' 20 | } 21 | if (patchWalkthrough.user_id !== uid) { 22 | return '您没有权限删除该攻略' 23 | } 24 | 25 | await prisma.patch_walkthrough.delete({ 26 | where: { id: input.walkthroughId } 27 | }) 28 | return {} 29 | } 30 | -------------------------------------------------------------------------------- /app/api/patch/walkthrough/update.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { prisma } from '~/prisma/index' 3 | import { updateWalkthroughSchema } from '~/validations/walkthrough' 4 | 5 | export const updateWalkthrough = async ( 6 | input: z.infer, 7 | uid: number 8 | ) => { 9 | const { walkthroughId, name, content } = input 10 | const walkthrough = await prisma.patch_walkthrough.findUnique({ 11 | where: { id: walkthroughId } 12 | }) 13 | if (!walkthrough) { 14 | return '未找到该攻略' 15 | } 16 | if (walkthrough.user_id !== uid) { 17 | return '您没有权限更改该攻略' 18 | } 19 | 20 | await prisma.patch_walkthrough.update({ 21 | where: { id: walkthroughId, user_id: uid }, 22 | data: { name, content } 23 | }) 24 | 25 | return {} 26 | } 27 | -------------------------------------------------------------------------------- /app/api/user/image/_upload.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | import { uploadImageToS3 } from '~/lib/s3/uploadImageToS3' 3 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 4 | 5 | export const uploadIntroductionImage = async ( 6 | name: string, 7 | image: ArrayBuffer, 8 | uid: number 9 | ) => { 10 | const minImage = await sharp(image) 11 | .resize(1920, 1080, { 12 | fit: 'inside', 13 | withoutEnlargement: true 14 | }) 15 | .avif({ quality: 30 }) 16 | .toBuffer() 17 | 18 | if (!checkBufferSize(minImage, 1.007)) { 19 | return '图片体积过大' 20 | } 21 | 22 | const bucketName = `user_${uid}/image` 23 | await uploadImageToS3(`${bucketName}/${name}.avif`, minImage) 24 | } 25 | -------------------------------------------------------------------------------- /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/setting/_upload.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | 3 | import { MAX_SIZE, COMPRESS_QUALITY } from '~/app/api/utils/constants' 4 | import { uploadImageToS3 } from '~/lib/s3/uploadImageToS3' 5 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 6 | 7 | export const uploadUserAvatar = async (image: ArrayBuffer, uid: number) => { 8 | const avatar = await sharp(image) 9 | .resize(256, 256, { 10 | fit: 'inside', 11 | withoutEnlargement: true 12 | }) 13 | .avif({ quality: COMPRESS_QUALITY }) 14 | .toBuffer() 15 | const miniAvatar = await sharp(image) 16 | .resize(100, 100, { 17 | fit: 'inside', 18 | withoutEnlargement: true 19 | }) 20 | .avif({ quality: COMPRESS_QUALITY }) 21 | .toBuffer() 22 | 23 | if (!checkBufferSize(avatar, MAX_SIZE)) { 24 | return '图片体积过大' 25 | } 26 | 27 | const bucketName = `user/avatar/user_${uid}` 28 | 29 | await uploadImageToS3(`${bucketName}/avatar.avif`, avatar) 30 | await uploadImageToS3(`${bucketName}/avatar-mini.avif`, miniAvatar) 31 | } 32 | -------------------------------------------------------------------------------- /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/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/constants.ts: -------------------------------------------------------------------------------- 1 | export const COMPRESS_QUALITY = 60 2 | export const MAX_SIZE = 1.007 3 | -------------------------------------------------------------------------------- /app/api/utils/generateRandomCode.ts: -------------------------------------------------------------------------------- 1 | export const generateRandomCode = (length: number) => { 2 | const charset = '023456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ' 3 | let code = '' 4 | for (let i = 0; i < length; i++) { 5 | const randomIndex = Math.floor(Math.random() * charset.length) 6 | code += charset[randomIndex] 7 | } 8 | return code 9 | } 10 | -------------------------------------------------------------------------------- /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/markdownToHtml.ts: -------------------------------------------------------------------------------- 1 | import rehypeSanitize from 'rehype-sanitize' 2 | import rehypeStringify from 'rehype-stringify' 3 | import remarkFrontmatter from 'remark-frontmatter' 4 | import remarkGfm from 'remark-gfm' 5 | import remarkParse from 'remark-parse' 6 | import remarkRehype from 'remark-rehype' 7 | import rehypePrism from 'rehype-prism-plus' 8 | import { unified } from 'unified' 9 | 10 | export const markdownToHtml = async (markdown: string) => { 11 | const htmlVFile = await unified() 12 | .use(remarkParse) 13 | .use(remarkRehype) 14 | .use(rehypeSanitize) 15 | .use(remarkFrontmatter) 16 | .use(remarkGfm) 17 | .use(rehypePrism, { ignoreMissing: true }) 18 | .use(rehypeStringify) 19 | .process(markdown) 20 | 21 | return String(htmlVFile) 22 | } 23 | -------------------------------------------------------------------------------- /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/uploadPatchBanner.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp' 2 | 3 | import { checkBufferSize } from '~/app/api/utils/checkBufferSize' 4 | import { MAX_SIZE, COMPRESS_QUALITY } from '~/app/api/utils/constants' 5 | import { uploadImageToS3 } from '~/lib/s3/uploadImageToS3' 6 | 7 | export const uploadPatchBanner = async (image: ArrayBuffer, id: number) => { 8 | const banner = await sharp(image) 9 | .resize(1920, 1080, { 10 | fit: 'inside', 11 | withoutEnlargement: true 12 | }) 13 | .avif({ quality: COMPRESS_QUALITY }) 14 | .toBuffer() 15 | const miniBanner = await sharp(image) 16 | .resize(460, 259, { 17 | fit: 'inside', 18 | withoutEnlargement: true 19 | }) 20 | .avif({ quality: COMPRESS_QUALITY }) 21 | .toBuffer() 22 | 23 | if (!checkBufferSize(miniBanner, MAX_SIZE)) { 24 | return '图片体积过大' 25 | } 26 | 27 | const bucketName = `patch/${id}/banner` 28 | 29 | await uploadImageToS3(`${bucketName}/banner.avif`, banner) 30 | await uploadImageToS3(`${bucketName}/banner-mini.avif`, miniBanner) 31 | } 32 | -------------------------------------------------------------------------------- /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 = 5 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/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/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/check-hash/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | 4 | export const kunMetadata: Metadata = { 5 | title: 'BLAKE3 文件校验 | 文件完好性校验', 6 | description: `您可以输入文件的 Hash, 然后上传文件以快速使用 BLAKE3 算法检查文件完好性`, 7 | openGraph: { 8 | title: 'BLAKE3 文件校验 | 文件完好性校验', 9 | description: `您可以输入文件的 Hash, 然后上传文件以快速使用 BLAKE3 算法检查文件完好性`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: 'BLAKE3 文件校验 | 文件完好性校验', 16 | description: `您可以输入文件的 Hash, 然后上传文件以快速使用 BLAKE3 算法检查文件完好性` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/check-hash` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/check-hash/page.tsx: -------------------------------------------------------------------------------- 1 | import { CheckHashContainer } from '~/components/check-hash/Container' 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 | -------------------------------------------------------------------------------- /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/company/[id]/metadata.ts: -------------------------------------------------------------------------------- 1 | import { kunMoyuMoe } from '~/config/moyu-moe' 2 | import type { Metadata } from 'next' 3 | import type { CompanyDetail } from '~/types/api/company' 4 | 5 | export const generateKunMetadataTemplate = ( 6 | company: CompanyDetail 7 | ): Metadata => { 8 | return { 9 | title: `所属会社为 ${company.name} 的 Galgame`, 10 | description: company.introduction, 11 | openGraph: { 12 | title: `所属会社为 ${company.name} 的 Galgame`, 13 | description: company.introduction, 14 | type: 'article', 15 | publishedTime: new Date(company.created).toISOString(), 16 | modifiedTime: new Date(company.created).toISOString(), 17 | tags: company.alias 18 | }, 19 | twitter: { 20 | card: 'summary', 21 | title: `所属会社为 ${company.name} 的 Galgame`, 22 | description: company.introduction 23 | }, 24 | alternates: { 25 | canonical: `${kunMoyuMoe.domain.main}/company/${company.id}` 26 | }, 27 | keywords: [company.name, ...company.alias] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/company/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getCompanySchema } from '~/validations/company' 6 | import { getCompany } from '~/app/api/company/all/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getCompanySchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getCompany(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/company/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).toString()} 下载`, 8 | openGraph: { 9 | title: `Galgame 会社分类`, 10 | description: `所有的 Galgame 会社分类, ${Object.values(SUPPORTED_TYPE_MAP).toString()} 下载`, 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).toString()} 下载` 18 | }, 19 | alternates: { 20 | canonical: `${kunMoyuMoe.domain.main}/company` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/company/page.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '~/components/company/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | import { kunMetadata } from './metadata' 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 | page: 1, 14 | limit: 100 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return ( 21 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /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 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /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/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 type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '资源消息' 5 | } 6 | -------------------------------------------------------------------------------- /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 = 5 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 type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = kunMetadata 7 | 8 | export default function MessageLayout({ 9 | children 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 |
15 | 19 |
20 | 21 |
{children}
22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /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 type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '通知消息' 5 | } 6 | -------------------------------------------------------------------------------- /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 = 5 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/patch-resource-create/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '新补丁资源发布消息' 5 | } 6 | -------------------------------------------------------------------------------- /app/message/patch-resource-create/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: 'patchResourceCreate', 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/message/patch-resource-update/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '补丁资源更新消息' 5 | } 6 | -------------------------------------------------------------------------------- /app/message/patch-resource-update/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: 'patchResourceUpdate', 14 | page: 1, 15 | limit: 30 16 | }) 17 | if (typeof response === 'string') { 18 | return 19 | } 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/message/system/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | export const kunMetadata: Metadata = { 4 | title: '系统消息' 5 | } 6 | -------------------------------------------------------------------------------- /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 = 5 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 = 5 5 | 6 | export default async function Kun() { 7 | const response = await kunGetActions() 8 | 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/patch/[id]/comment/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getPatchComment } from '~/app/api/patch/comment/get' 5 | import { getCommentVerifyStatus } from '~/app/api/admin/setting/comment/getCommentVerifyStatus' 6 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 7 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 8 | 9 | const patchIdSchema = z.object({ 10 | patchId: z.coerce.number().min(1).max(9999999) 11 | }) 12 | 13 | export const kunGetCommentActions = async ( 14 | params: z.infer 15 | ) => { 16 | const input = safeParseSchema(patchIdSchema, params) 17 | if (typeof input === 'string') { 18 | return input 19 | } 20 | const payload = await verifyHeaderCookie() 21 | 22 | const response = await getPatchComment(input, payload?.uid ?? 0) 23 | return response 24 | } 25 | 26 | export const kunGetCommentVerifyStatusActions = async () => { 27 | const response = await getCommentVerifyStatus() 28 | return response 29 | } 30 | -------------------------------------------------------------------------------- /app/patch/[id]/history/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getPatchHistorySchema } from '~/validations/patch' 5 | import { getPatchHistory } from '~/app/api/patch/history/route' 6 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getPatchHistorySchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getPatchHistory(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/patch/[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(`/patch/${id}/introduction`) 10 | } 11 | -------------------------------------------------------------------------------- /app/patch/[id]/pr/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getPullRequest } from '~/app/api/patch/pr/route' 6 | 7 | const patchIdSchema = z.object({ 8 | patchId: z.coerce.number().min(1).max(9999999) 9 | }) 10 | 11 | export const kunGetActions = async (params: z.infer) => { 12 | const input = safeParseSchema(patchIdSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | 17 | const response = await getPullRequest(input) 18 | return response 19 | } 20 | -------------------------------------------------------------------------------- /app/patch/[id]/resource/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { verifyHeaderCookie } from '~/utils/actions/verifyHeaderCookie' 6 | import { getPatchResource } from '~/app/api/patch/resource/get' 7 | 8 | const patchIdSchema = z.object({ 9 | patchId: z.coerce.number().min(1).max(9999999) 10 | }) 11 | 12 | export const kunGetActions = async (params: z.infer) => { 13 | const input = safeParseSchema(patchIdSchema, params) 14 | if (typeof input === 'string') { 15 | return input 16 | } 17 | const payload = await verifyHeaderCookie() 18 | 19 | const response = await getPatchResource(input, payload?.uid ?? 0) 20 | return response 21 | } 22 | -------------------------------------------------------------------------------- /app/patch/[id]/walkthrough/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getWalkthrough } from '~/app/api/patch/walkthrough/get' 6 | 7 | const patchIdSchema = z.object({ 8 | patchId: z.coerce.number().min(1).max(9999999) 9 | }) 10 | 11 | export const kunGetActions = async (params: z.infer) => { 12 | const input = safeParseSchema(patchIdSchema, params) 13 | if (typeof input === 'string') { 14 | return input 15 | } 16 | 17 | const response = await getWalkthrough(input) 18 | return response 19 | } 20 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { AppProgressBar, useRouter } from 'next-nprogress-bar' 4 | import { NextUIProvider } from '@nextui-org/react' 5 | import { ThemeProvider } from 'next-themes' 6 | 7 | export const Providers = ({ children }: { children: React.ReactNode }) => { 8 | const router = useRouter() 9 | 10 | return ( 11 | 12 | {children} 13 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/ranking/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default function Kun() { 4 | redirect('/ranking/user') 5 | } 6 | -------------------------------------------------------------------------------- /app/ranking/patch/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}/ranking/patch` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/ranking/patch/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPatchRanking } from '~/app/ranking/actions' 2 | import { RankingContainer } from '~/components/ranking/Container' 3 | import { PatchList } from '~/components/ranking/patch/PatchList' 4 | import { kunMetadata } from './metadata' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 5 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | interface Props { 12 | searchParams: Promise<{ sortBy?: string; timeRange?: string }> 13 | } 14 | 15 | export default async function Kun({ searchParams }: Props) { 16 | const res = await searchParams 17 | 18 | const sortBy = res.sortBy || 'view' 19 | const timeRange = res.timeRange || 'all' 20 | const patches = await getPatchRanking(sortBy, timeRange) 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/ranking/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: `所有发布 Galgame 以及 Galgame 资源的用户排行, 可以按照 萌萌点, Galgame, Galgame 补丁, 评论 进行排序, 查看用户的活跃和贡献排行, 查看最值得信赖的用户, 查看最强的 Galgame 补丁创作者`, 7 | openGraph: { 8 | title: '用户排行榜', 9 | description: `所有发布 Galgame 以及 Galgame 资源的用户排行, 可以按照 萌萌点, Galgame, Galgame 补丁, 评论 进行排序, 查看用户的活跃和贡献排行, 查看最值得信赖的用户, 查看最强的 Galgame 补丁创作者`, 10 | type: 'website', 11 | images: kunMoyuMoe.images 12 | }, 13 | twitter: { 14 | card: 'summary_large_image', 15 | title: '用户排行榜', 16 | description: `所有发布 Galgame 以及 Galgame 资源的用户排行, 可以按照 萌萌点, Galgame, Galgame 补丁, 评论 进行排序, 查看用户的活跃和贡献排行, 查看最值得信赖的用户, 查看最强的 Galgame 补丁创作者` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/ranking/patch` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/ranking/user/page.tsx: -------------------------------------------------------------------------------- 1 | import { getUserRanking } from '~/app/ranking/actions' 2 | import { RankingContainer } from '~/components/ranking/Container' 3 | import { UserList } from '~/components/ranking/user/UserList' 4 | import { kunMetadata } from './metadata' 5 | import type { Metadata } from 'next' 6 | 7 | export const revalidate = 5 8 | 9 | export const metadata: Metadata = kunMetadata 10 | 11 | interface Props { 12 | searchParams: Promise<{ sortBy?: string; timeRange?: string }> 13 | } 14 | 15 | export default async function Kun({ searchParams }: Props) { 16 | const res = await searchParams 17 | 18 | const sortBy = res.sortBy || 'moemoepoint' 19 | const timeRange = res.timeRange || 'all' 20 | const users = await getUserRanking(sortBy) 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/release/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 5 | import { getReleaseSchema } from '~/validations/release' 6 | import { getGalgameRelease } from '~/app/api/release/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getReleaseSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getGalgameRelease(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/release/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 时间表, 最新 Galgame 新作分类, Galgame 新作资源下载, Galgame 新作大全`, 7 | openGraph: { 8 | title: 'Galgame 新作时间表', 9 | description: `本月发售的最新 Galgame 列表, Galgame 新作, Galgame 时间表, 最新 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 时间表, 最新 Galgame 新作分类, Galgame 新作资源下载, Galgame 新作大全` 17 | }, 18 | alternates: { 19 | canonical: `${kunMoyuMoe.domain.main}/release` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/release/page.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { ReleaseContainer } from '~/components/release/Container' 3 | import { kunGetActions } from './actions' 4 | import { ErrorComponent } from '~/components/error/ErrorComponent' 5 | import { kunMetadata } from './metadata' 6 | import type { Metadata } from 'next' 7 | 8 | export const revalidate = 5 9 | 10 | export const metadata: Metadata = kunMetadata 11 | 12 | export default async function Kun() { 13 | const currentYear = dayjs().year() 14 | const currentMonth = dayjs().month() + 1 15 | 16 | const response = await kunGetActions({ 17 | year: currentYear, 18 | month: currentMonth 19 | }) 20 | if (typeof response === 'string') { 21 | return 22 | } 23 | 24 | return ( 25 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /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/Container' 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]/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 | return { 7 | ...generateNullMetadata(`标签 - ${tag.name}`) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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 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 | page: 1, 14 | limit: 100 15 | }) 16 | if (typeof response === 'string') { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /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 kunGetUserStatusActions = 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 { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserComment } from '~/app/api/user/profile/comment/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getUserInfoSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserComment(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/user/[id]/comment/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('评论') 5 | } 6 | -------------------------------------------------------------------------------- /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 { generateKunMetadataTemplate } from './metadata' 5 | 6 | export const revalidate = 5 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export const generateMetadata = () => { 13 | return generateKunMetadataTemplate 14 | } 15 | 16 | export default async function Kun({ params }: Props) { 17 | const { id } = await params 18 | 19 | const response = await kunGetActions({ 20 | uid: Number(id), 21 | page: 1, 22 | limit: 20 23 | }) 24 | if (typeof response === 'string') { 25 | return 26 | } 27 | 28 | return ( 29 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/user/[id]/contribute/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getUserInfoSchema } from '~/validations/user' 5 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserContribute } from '~/app/api/user/profile/contribute/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getUserInfoSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserContribute(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/user/[id]/contribute/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('贡献过的 Galgame') 5 | } 6 | -------------------------------------------------------------------------------- /app/user/[id]/contribute/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserContribute } from '~/components/user/contribute/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | import { generateKunMetadataTemplate } from './metadata' 5 | 6 | export const revalidate = 5 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export const generateMetadata = () => { 13 | return generateKunMetadataTemplate 14 | } 15 | 16 | export default async function Kun({ params }: Props) { 17 | const { id } = await params 18 | 19 | const response = await kunGetActions({ 20 | uid: Number(id), 21 | page: 1, 22 | limit: 20 23 | }) 24 | if (typeof response === 'string') { 25 | return 26 | } 27 | 28 | return ( 29 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/user/[id]/favorite/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getUserInfoSchema } from '~/validations/user' 5 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserFavorite } from '~/app/api/user/profile/favorite/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getUserInfoSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserFavorite(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/user/[id]/favorite/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('收藏补丁') 5 | } 6 | -------------------------------------------------------------------------------- /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 | import { generateKunMetadataTemplate } from './metadata' 5 | 6 | export const revalidate = 5 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export const generateMetadata = () => { 13 | return generateKunMetadataTemplate 14 | } 15 | 16 | export default async function Kun({ params }: Props) { 17 | const { id } = await params 18 | 19 | const response = await kunGetActions({ 20 | uid: Number(id), 21 | page: 1, 22 | limit: 20 23 | }) 24 | if (typeof response === 'string') { 25 | return 26 | } 27 | 28 | return ( 29 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/user/[id]/galgame/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { z } from 'zod' 4 | import { getUserInfoSchema } from '~/validations/user' 5 | import { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserGalgame } from '~/app/api/user/profile/galgame/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getUserInfoSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserGalgame(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/user/[id]/galgame/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('Galgame 项目') 5 | } 6 | -------------------------------------------------------------------------------- /app/user/[id]/galgame/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserGalgame } from '~/components/user/galgame/Container' 2 | import { kunGetActions } from './actions' 3 | import { ErrorComponent } from '~/components/error/ErrorComponent' 4 | import { generateKunMetadataTemplate } from './metadata' 5 | 6 | export const revalidate = 5 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export const generateMetadata = () => { 13 | return generateKunMetadataTemplate 14 | } 15 | 16 | export default async function Kun({ params }: Props) { 17 | const { id } = await params 18 | 19 | const response = await kunGetActions({ 20 | uid: Number(id), 21 | page: 1, 22 | limit: 20 23 | }) 24 | if (typeof response === 'string') { 25 | return 26 | } 27 | 28 | return ( 29 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/user/[id]/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('用户主页') 5 | } 6 | -------------------------------------------------------------------------------- /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 { safeParseSchema } from '~/utils/actions/safeParseSchema' 6 | import { getUserPatchResource } from '~/app/api/user/profile/resource/route' 7 | 8 | export const kunGetActions = async ( 9 | params: z.infer 10 | ) => { 11 | const input = safeParseSchema(getUserInfoSchema, params) 12 | if (typeof input === 'string') { 13 | return input 14 | } 15 | 16 | const response = await getUserPatchResource(input) 17 | return response 18 | } 19 | -------------------------------------------------------------------------------- /app/user/[id]/resource/metadata.ts: -------------------------------------------------------------------------------- 1 | import { generateNullMetadata } from '~/utils/noIndex' 2 | 3 | export const generateKunMetadataTemplate = { 4 | ...generateNullMetadata('补丁资源') 5 | } 6 | -------------------------------------------------------------------------------- /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 { generateKunMetadataTemplate } from './metadata' 5 | 6 | export const revalidate = 5 7 | 8 | interface Props { 9 | params: Promise<{ id: string }> 10 | } 11 | 12 | export const generateMetadata = () => { 13 | return generateKunMetadataTemplate 14 | } 15 | 16 | export default async function Kun({ params }: Props) { 17 | const { id } = await params 18 | 19 | const response = await kunGetActions({ 20 | uid: Number(id), 21 | page: 1, 22 | limit: 20 23 | }) 24 | if (typeof response === 'string') { 25 | return 26 | } 27 | 28 | return ( 29 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/user/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default async function Kun() { 4 | redirect('/') 5 | } 6 | -------------------------------------------------------------------------------- /components/about/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 | 13 | 14 | {/* } 21 | onChange={(e) => onSearch(e.target.value)} 22 | /> */} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/about/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/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/setting/Container.tsx: -------------------------------------------------------------------------------- 1 | import { DisableRegisterSetting } from './DisableRegisterSetting' 2 | import { EnableCommentVerify } from './EnableCommentVerify' 3 | import { EnableOnlyCreatorCreateGalgame } from './EnableOnlyCreatorCreateGalgame' 4 | 5 | interface Props { 6 | disableRegister: boolean 7 | enableCommentVerify: boolean 8 | enableOnlyCreatorCreate: boolean 9 | } 10 | 11 | export const AdminSetting = ({ 12 | disableRegister, 13 | enableCommentVerify, 14 | enableOnlyCreatorCreate 15 | }: Props) => { 16 | return ( 17 |
18 |
19 |

网站设置

20 |
21 | 22 | 23 | 24 | 25 | 26 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /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/check-hash/Container.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { CheckHash } from './CheckHash' 3 | import { KunHeader } from '../kun/Header' 4 | 5 | export const CheckHashContainer = () => { 6 | return ( 7 | 8 |
9 | 13 | 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/comment/_sort.d.ts: -------------------------------------------------------------------------------- 1 | export type SortOption = 'created' | 'like' 2 | export type SortDirection = 'asc' | 'desc' 3 | -------------------------------------------------------------------------------- /components/company/CompanyList.tsx: -------------------------------------------------------------------------------- 1 | import { KunMasonryGrid } from '~/components/kun/MasonryGrid' 2 | import { KunLoading } from '~/components/kun/Loading' 3 | import { KunNull } from '~/components/kun/Null' 4 | import { CompanyCard } from './Card' 5 | import type { FC } from 'react' 6 | import type { Company as CompanyType } from '~/types/api/company' 7 | 8 | interface CompanyListProps { 9 | companies: CompanyType[] 10 | loading: boolean 11 | searching: boolean 12 | } 13 | 14 | export const CompanyList: FC = ({ 15 | companies, 16 | loading, 17 | searching 18 | }) => { 19 | if (loading) { 20 | return 21 | } 22 | 23 | if (!searching && companies.length === 0) { 24 | return 25 | } 26 | 27 | return ( 28 | 29 | {companies.map((company) => ( 30 | 31 | ))} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/company/form/LogoImage.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { dataURItoBlob } from '~/utils/dataURItoBlob' 4 | import { KunImageCropper } from '~/components/kun/cropper/KunImageCropper' 5 | import type { FC } from 'react' 6 | 7 | interface Props { 8 | initialUrl: string 9 | setInitialUrl: (url: string) => void 10 | setImageBlob: (imageBlob: Blob | null) => void 11 | } 12 | 13 | export const LogoImage: FC = ({ 14 | initialUrl, 15 | setInitialUrl, 16 | setImageBlob 17 | }) => { 18 | const handleImageComplete = async (croppedImage: string) => { 19 | const imageBlob = dataURItoBlob(croppedImage) 20 | setInitialUrl('') 21 | setImageBlob(imageBlob) 22 | } 23 | 24 | const removeImage = () => { 25 | setInitialUrl('') 26 | setImageBlob(null) 27 | } 28 | 29 | return ( 30 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /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 | description: string 14 | } 15 | 16 | export interface VNDB { 17 | title: string 18 | titles: Title[] 19 | description: string 20 | aliases: string[] 21 | released: string 22 | } 23 | 24 | export interface VNDBResponse { 25 | more: boolean 26 | results: VNDB[] 27 | } 28 | -------------------------------------------------------------------------------- /components/edit/rewrite/GameNameInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@nextui-org/input' 2 | 3 | interface Props { 4 | name: string 5 | onChange: (newName: string) => void 6 | error?: string 7 | } 8 | 9 | export const GameNameInput = ({ name, onChange, error }: Props) => ( 10 |
11 |

游戏名称 (必须)

12 | onChange(e.target.value)} 20 | isInvalid={!!error} 21 | errorMessage={error} 22 | /> 23 |
24 | ) 25 | -------------------------------------------------------------------------------- /components/edit/rewrite/VNDBInput.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Input } from '@nextui-org/react' 4 | 5 | interface Props { 6 | vndbId: string 7 | setVNDBId: (vndbId: string) => void 8 | errors?: string 9 | } 10 | 11 | export const VNDBInput = ({ vndbId, setVNDBId, errors }: Props) => { 12 | return ( 13 |
14 |

VNDB ID (可选)

15 | setVNDBId(e.target.value)} 21 | isInvalid={!!errors} 22 | errorMessage={errors} 23 | /> 24 |

25 | 提示: VNDB ID 需要 VNDB 官网 (vndb.org) 26 | 获取,当进入对应游戏的页面,游戏页面的 URL (形如 27 | https://vndb.org/v19658) 中的 v19658 就是 VNDB ID 28 |

29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /components/galgame/_sort.d.ts: -------------------------------------------------------------------------------- 1 | export type SortOption = 2 | | 'resource_update_time' 3 | | 'created' 4 | | 'view' 5 | | 'download' 6 | export type SortDirection = 'asc' | 'desc' 7 | -------------------------------------------------------------------------------- /components/home/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { KunCarousel } from './carousel/KunCarousel' 2 | import { getKunPosts } from './carousel/mdx' 3 | 4 | export const HomeHero = () => { 5 | const posts = getKunPosts() 6 | 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /components/kun/BackToTop.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@nextui-org/react' 4 | import { ArrowUp } from 'lucide-react' 5 | import { useEffect, useState } from 'react' 6 | 7 | export const KunBackToTop = () => { 8 | const [show, setShow] = useState(false) 9 | 10 | useEffect(() => { 11 | const handleScroll = () => { 12 | setShow(window.scrollY > 400) 13 | } 14 | 15 | window.addEventListener('scroll', handleScroll) 16 | return () => window.removeEventListener('scroll', handleScroll) 17 | }, []) 18 | 19 | const scrollToTop = () => { 20 | window.scrollTo({ 21 | top: 0, 22 | behavior: 'smooth' 23 | }) 24 | } 25 | 26 | if (!show) { 27 | return null 28 | } 29 | 30 | return ( 31 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /components/kun/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from '@nextui-org/spinner' 2 | import { cn } from '~/utils/cn' 3 | 4 | interface Props { 5 | hint: string 6 | className?: string 7 | } 8 | 9 | export const KunLoading = ({ hint, className }: Props) => { 10 | return ( 11 |
17 | 18 | {hint} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/kun/NSFWIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from '@nextui-org/alert' 2 | 3 | interface Props { 4 | isNSFWEnable: boolean 5 | } 6 | 7 | export const NSFWIndicator = ({ isNSFWEnable }: Props) => { 8 | const titleString = isNSFWEnable ? 'NSFW 已启用' : 'NSFW 未启用' 9 | const description = isNSFWEnable 10 | ? '网站已启用 NSFW, 请注意您周围没有人' 11 | : '网站未启用 NSFW, 部分补丁内容不可见, 请在网站右上角打开 NSFW' 12 | 13 | return ( 14 |
15 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/kun/Null.tsx: -------------------------------------------------------------------------------- 1 | import { randomNum } from '~/utils/random' 2 | import { useEffect, useState } from 'react' 3 | import { KunLoading } from './Loading' 4 | import Image from 'next/image' 5 | 6 | interface Props { 7 | message: string 8 | } 9 | 10 | export const KunNull = ({ message }: Props) => { 11 | const [stickerSrc, setStickerSrc] = useState('') 12 | 13 | useEffect(() => { 14 | const randomPackIndex = randomNum(1, 5) 15 | const randomStickerIndex = randomNum(1, 80) 16 | setStickerSrc( 17 | `https://sticker.kungal.com/stickers/KUNgal${randomPackIndex}/${randomStickerIndex}.webp` 18 | ) 19 | }, []) 20 | 21 | if (!stickerSrc) { 22 | return 23 | } 24 | 25 | return ( 26 |
27 | {message} 35 | {message} 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /components/kun/TextDivider.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from '@nextui-org/divider' 2 | 3 | interface Props { 4 | text: string 5 | } 6 | 7 | export const KunTextDivider = ({ text }: Props) => { 8 | return ( 9 |
10 | 11 | {text} 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/kun/auth/captcha.d.ts: -------------------------------------------------------------------------------- 1 | export interface KunCaptchaImage { 2 | id: string 3 | data: string 4 | isWhiteHair: boolean 5 | } 6 | 7 | export interface AuthFormData { 8 | email: string 9 | password: string 10 | } 11 | 12 | export interface CaptchaResponse { 13 | images: KunCaptchaImage[] 14 | sessionId: string 15 | } 16 | 17 | export interface CaptchaVerifyRequest { 18 | sessionId: string 19 | selectedIds: string[] 20 | } 21 | 22 | export interface CaptchaVerifyResponse { 23 | success: boolean 24 | message: string 25 | } 26 | -------------------------------------------------------------------------------- /components/kun/cropper/KunMosaicController.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from '@nextui-org/react' 2 | import { FC } from 'react' 3 | 4 | interface KunMosaicControllerProps { 5 | mosaicSize: number 6 | onMosaicSizeChange: (size: number) => void 7 | } 8 | 9 | export const KunMosaicController: FC = ({ 10 | mosaicSize, 11 | onMosaicSizeChange 12 | }) => { 13 | return ( 14 |
15 |
16 | 17 | onMosaicSizeChange(Number(value))} 24 | className="max-w-md" 25 | label="单位:像素" 26 | /> 27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /components/kun/cropper/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface KunAspect { 2 | x: number 3 | y: number 4 | } 5 | -------------------------------------------------------------------------------- /components/kun/floating-card/KunUserStatCard.tsx: -------------------------------------------------------------------------------- 1 | interface UserStatCardProps { 2 | value: number 3 | label: string 4 | } 5 | 6 | export const KunUserStatCard = ({ value, label }: UserStatCardProps) => { 7 | return ( 8 |
9 |

{value.toLocaleString()}

10 |

{label}

11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /components/kun/icons/GitHub.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { SVGProps } from 'react' 3 | 4 | export const GitHub = (props: SVGProps) => { 5 | return ( 6 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/kun/icons/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { SVGProps } from 'react' 3 | 4 | export const Markdown = (props: SVGProps) => { 5 | return ( 6 | 13 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/kun/icons/Telegram.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { SVGProps } from 'react' 3 | 4 | export const Telegram = (props: SVGProps) => { 5 | return ( 6 | 13 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/kun/milkdown/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { KunEditorProvider } from './EditorProvider' 2 | import { MilkdownProvider } from '@milkdown/react' 3 | import { ProsemirrorAdapterProvider } from '@prosemirror-adapter/react' 4 | 5 | interface KunEditorProps { 6 | valueMarkdown: string 7 | saveMarkdown: (markdown: string) => void 8 | placeholder?: string 9 | } 10 | 11 | export const KunEditor = (props: KunEditorProps) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/kun/milkdown/plugins/MenuButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from '@nextui-org/react' 2 | import { FC } from 'react' 3 | import { LucideIcon } from 'lucide-react' 4 | 5 | interface MenuButtonProps { 6 | tooltip: string 7 | icon: LucideIcon 8 | onPress: () => void 9 | ariaLabel: string 10 | } 11 | 12 | export const MenuButton: FC = ({ 13 | tooltip, 14 | icon: Icon, 15 | onPress, 16 | ariaLabel 17 | }) => ( 18 | 19 | 22 | 23 | ) 24 | -------------------------------------------------------------------------------- /components/kun/top-bar/Search.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Tooltip } from '@nextui-org/tooltip' 4 | import { Button, Link } from '@nextui-org/react' 5 | import { Search } from 'lucide-react' 6 | import { useRouter } from 'next-nprogress-bar' 7 | import { useHotkeys } from 'react-hotkeys-hook' 8 | 9 | export const KunSearch = () => { 10 | const router = useRouter() 11 | useHotkeys('ctrl+k', (event) => { 12 | event.preventDefault() 13 | router.push('/search') 14 | }) 15 | 16 | return ( 17 | 23 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/kun/utils/loli.ts: -------------------------------------------------------------------------------- 1 | import { randomNum } from '~/utils/random' 2 | 3 | const getAssetsFile = (name: string) => `/sooner/${name}.webp` 4 | 5 | const number = randomNum(0, 3) 6 | 7 | let loli = '' 8 | let name = '' 9 | 10 | if (number === 0) { 11 | // Actually, her full name is: アーデルハイト・フォン・ベルクシュトラーセ 12 | name = 'あーちゃん' 13 | loli = getAssetsFile(name) 14 | } else if (number === 1) { 15 | name = 'こじかひわ' 16 | loli = getAssetsFile(name) 17 | } else if (number === 2) { 18 | name = '雪々' 19 | loli = getAssetsFile(name) 20 | } else { 21 | name = '琥珀' 22 | loli = getAssetsFile(name) 23 | } 24 | 25 | export const loliAttribute = { loli, name } 26 | -------------------------------------------------------------------------------- /components/patch/Contributor.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, CardHeader } from '@nextui-org/card' 2 | import { AvatarGroup } from '@nextui-org/avatar' 3 | import { KunAvatar } from '~/components/kun/floating-card/KunAvatar' 4 | 5 | interface Props { 6 | users: KunUser[] 7 | } 8 | 9 | export const PatchContributor = ({ users }: Props) => { 10 | return ( 11 | 12 | 13 |

贡献者

14 |
15 | 16 |

感谢下面的朋友们为本补丁信息做出的贡献

17 | 18 | {users.map((user) => ( 19 | 28 | ))} 29 | 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /components/patch/_diff.scss: -------------------------------------------------------------------------------- 1 | .kun-content-added { 2 | display: inline; 3 | color: #17c964; 4 | font-weight: bold; 5 | } 6 | 7 | .kun-content-deleted { 8 | display: inline; 9 | color: #f31260; 10 | text-decoration: line-through; 11 | font-weight: bold; 12 | } 13 | 14 | .kun-content-modified { 15 | display: inline; 16 | } 17 | -------------------------------------------------------------------------------- /components/patch/comment/_scrollIntoComment.ts: -------------------------------------------------------------------------------- 1 | export const scrollIntoComment = (id: number | null) => { 2 | if (id === null) { 3 | return 4 | } 5 | 6 | const targetComment = document.getElementById(`comment-${id}`) 7 | if (targetComment) { 8 | targetComment.scrollIntoView({ behavior: 'smooth', block: 'center' }) 9 | targetComment.classList.add('bg-default-100') 10 | setTimeout(() => { 11 | targetComment.classList.remove('bg-default-100') 12 | }, 2000) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /components/patch/introduction/Container.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, CardHeader } from '@nextui-org/card' 2 | import { Info } from './Info' 3 | import { PatchTag } from './Tag' 4 | import { PatchCompany } from './Company' 5 | import type { PatchIntroduction } from '~/types/api/patch' 6 | 7 | interface Props { 8 | intro: PatchIntroduction 9 | patchId: number 10 | } 11 | 12 | export const InfoContainer = ({ intro, patchId }: Props) => { 13 | return ( 14 | 15 | 16 |

游戏介绍

17 |
18 | 19 | 20 | 21 | {/*
22 |

游戏制作商

23 |
*/} 24 | 25 | 26 | 27 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /components/patch/resource/share.d.ts: -------------------------------------------------------------------------------- 1 | import type { Control, FieldErrors } from 'react-hook-form' 2 | import type { PatchResourceLink } from '~/types/api/patch' 3 | 4 | interface Fields { 5 | type: string[] 6 | name: string 7 | modelName: string 8 | patchId: number 9 | code: string 10 | storage: string 11 | hash: string 12 | content: string 13 | size: string 14 | password: string 15 | note: string 16 | language: string[] 17 | platform: string[] 18 | } 19 | 20 | // uploadStatus: 1 - uploading, 2 - merging, 3 - complete, 4 - error 21 | export interface FileStatus { 22 | file: File 23 | progress: number 24 | uploadStatus: number 25 | error?: string 26 | hash?: string 27 | filetype?: string 28 | } 29 | 30 | export type ErrorType = FieldErrors 31 | export type ControlType = Control 32 | -------------------------------------------------------------------------------- /components/ranking/patch/PatchList.tsx: -------------------------------------------------------------------------------- 1 | import { GalgameCard } from '~/components/galgame/Card' 2 | 3 | interface Props { 4 | patches: GalgameCard[] 5 | } 6 | 7 | export const PatchList = ({ patches }: Props) => { 8 | return ( 9 |
10 | {patches.map((patch) => ( 11 | 12 | ))} 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/ranking/user/UserList.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { UserCard } from './UserCard' 4 | import type { RankingUser } from '~/types/api/ranking' 5 | 6 | interface Props { 7 | users: RankingUser[] 8 | sortBy?: string 9 | } 10 | 11 | export const UserList = ({ 12 | users, 13 | sortBy = 'moemoepoint_increment' 14 | }: Props) => { 15 | return ( 16 |
17 | {users.map((user, index) => ( 18 | 19 | ))} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/ranking/user/UserStatsItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Tooltip } from '@nextui-org/tooltip' 4 | import { Badge } from '@nextui-org/badge' 5 | import { formatNumber } from '~/utils/formatNumber' 6 | 7 | export const UserStatsItem = ({ 8 | icon, 9 | value, 10 | increment, 11 | label 12 | }: { 13 | icon: React.ReactNode 14 | value: number 15 | increment: number 16 | label: string 17 | }) => { 18 | const incrementLabel = `${increment > 0 ? '+' : ''}${formatNumber(increment)}` 19 | return ( 20 | 27 | 28 |
29 | {icon} 30 | {value} 31 |
32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/resource/_sort.d.ts: -------------------------------------------------------------------------------- 1 | export type SortOption = 'update_time' | 'created' | 'download' | 'like' 2 | export type SortDirection = 'asc' | 'desc' 3 | -------------------------------------------------------------------------------- /components/settings/Nav.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/components/settings/Nav.tsx -------------------------------------------------------------------------------- /components/settings/user/Avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardBody, CardFooter } from '@nextui-org/card' 4 | import { AvatarCrop } from './AvatarCrop' 5 | 6 | export const UserAvatar = () => { 7 | return ( 8 | 9 | 10 |
11 |

头像

12 |

这是您的头像设置

13 |

您可以点击头像以上传图片文件

14 |
15 | 16 | 17 |
18 | 19 | 20 |

21 | 头像不是必须, 但是我们强烈推荐设置头像 22 |

23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /components/settings/user/Container.tsx: -------------------------------------------------------------------------------- 1 | import { KunHeader } from '~/components/kun/Header' 2 | import { UserAvatar } from './Avatar' 3 | import { Username } from './Username' 4 | import { Bio } from './Bio' 5 | import { Email } from './Email' 6 | import { Password } from './Password' 7 | import { MessageSettings } from './MessageSettings' 8 | import { Reset } from './Reset' 9 | 10 | export const UserSettings = () => { 11 | return ( 12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /components/tag/TagHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@nextui-org/button' 4 | import { useDisclosure } from '@nextui-org/modal' 5 | import { Plus } from 'lucide-react' 6 | import { CreateTagModal } from '~/components/tag/CreateTagModal' 7 | import { KunHeader } from '../kun/Header' 8 | import type { Tag as TagType } from '~/types/api/tag' 9 | 10 | interface Props { 11 | setNewTag: (tag: TagType) => void 12 | } 13 | 14 | export const TagHeader = ({ setNewTag }: Props) => { 15 | const { isOpen, onOpen, onClose } = useDisclosure() 16 | 17 | return ( 18 | <> 19 | }> 24 | 创建标签 25 | 26 | } 27 | /> 28 | 29 | { 33 | setNewTag(newTag) 34 | onClose() 35 | }} 36 | /> 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /components/tag/TagList.tsx: -------------------------------------------------------------------------------- 1 | import { KunMasonryGrid } from '~/components/kun/MasonryGrid' 2 | import { TagCard } from './Card' 3 | import { KunNull } from '~/components/kun/Null' 4 | import { KunLoading } from '~/components/kun/Loading' 5 | import type { Tag as TagType } from '~/types/api/tag' 6 | 7 | interface TagListProps { 8 | tags: TagType[] 9 | loading: boolean 10 | searching: boolean 11 | } 12 | 13 | export const TagList = ({ tags, loading, searching }: TagListProps) => { 14 | if (loading) { 15 | return 16 | } 17 | 18 | if (!searching && tags.length === 0) { 19 | return 20 | } 21 | 22 | return ( 23 | 24 | {tags.map((tag) => ( 25 | 26 | ))} 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/user/contribute/Card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody } from '@nextui-org/card' 2 | import { formatDate } from '~/utils/time' 3 | import Link from 'next/link' 4 | import type { UserContribute } from '~/types/api/user' 5 | 6 | interface Props { 7 | contribute: UserContribute 8 | } 9 | 10 | export const UserContributeCard = ({ contribute }: Props) => { 11 | return ( 12 | 19 | 20 |

21 | {contribute.patchName} 22 |

23 |

24 | 贡献于{' '} 25 | {formatDate(contribute.created, { 26 | isPrecise: true, 27 | isShowYear: true 28 | })} 29 |

30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /config/config.d.ts: -------------------------------------------------------------------------------- 1 | export interface KunSiteDomain { 2 | main: string 3 | imageBed: string 4 | storage: string 5 | kungal: string 6 | telegram_group: string 7 | cluster: string 8 | } 9 | 10 | export interface KunSiteAuthor { 11 | name: string 12 | url: string 13 | } 14 | 15 | export interface KunSiteOpenGraph { 16 | title: string 17 | description: string 18 | image: string 19 | url: string 20 | } 21 | 22 | export interface KunSiteCreator { 23 | name: string 24 | mention: string 25 | url: string 26 | } 27 | 28 | export interface KunSiteImage { 29 | url: string 30 | width: number 31 | height: number 32 | alt: string 33 | } 34 | 35 | export interface KunSiteConfig { 36 | title: string 37 | titleShort: string 38 | template: string 39 | description: string 40 | keywords: string[] 41 | canonical: string 42 | author: KunSiteAuthor[] 43 | creator: KunSiteCreator 44 | publisher: KunSiteCreator 45 | domain: KunSiteDomain 46 | og: KunSiteOpenGraph 47 | images: KunSiteImage[] 48 | } 49 | -------------------------------------------------------------------------------- /config/friend.ts: -------------------------------------------------------------------------------- 1 | import friendsData from './friend.json' 2 | 3 | interface KunFriend { 4 | name: string 5 | avatar: string 6 | label: string 7 | link: string 8 | } 9 | 10 | export const kunFriends: KunFriend[] = friendsData.friends 11 | -------------------------------------------------------------------------------- /config/redis.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_DELETE_EMAIL_CACHE_KEY = 'admin:delete:user:email' 2 | export const ADMIN_DELETE_IP_CACHE_KEY = 'admin:delete:user:ip' 3 | 4 | export const KUN_PATCH_DISABLE_REGISTER_KEY = 'admin:setting:register' 5 | 6 | export const KUN_PATCH_ENABLE_COMMENT_VERIFY_KEY = 7 | 'admin:setting:comment:verify' 8 | 9 | export const KUN_PATCH_ENABLE_ONLY_CREATOR_CREATE_KEY = 10 | 'admin:setting:creator:create' 11 | -------------------------------------------------------------------------------- /config/search.ts: -------------------------------------------------------------------------------- 1 | export const KUN_PATCH_MAX_HISTORY_ITEMS = 10 2 | -------------------------------------------------------------------------------- /config/upload.ts: -------------------------------------------------------------------------------- 1 | export const UPLOAD_IMAGE_COMPRESS_QUALITY = 60 2 | export const UPLOAD_IMAGE_MAX_SIZE = 1.007 3 | 4 | export const CHUNK_SIZE = 20 * 1024 * 1024 5 | export const UPLOAD_DIR = 'uploads/resource' 6 | 7 | export const MAX_SMALL_FILE_SIZE = 200 * 1024 * 1024 8 | export const MAX_LARGE_FILE_SIZE = 1 * 1024 * 1024 * 1024 9 | 10 | export const USER_DAILY_UPLOAD_LIMIT = 100 * 1024 * 1024 11 | export const CREATOR_DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 * 1024 12 | -------------------------------------------------------------------------------- /config/user.ts: -------------------------------------------------------------------------------- 1 | export const KUN_PATCH_USER_DAILY_UPLOAD_IMAGE_LIMIT = 200 2 | -------------------------------------------------------------------------------- /constants/about.ts: -------------------------------------------------------------------------------- 1 | export const aboutDirectoryLabelMap: Record = { 2 | about: '关于我们', 3 | dev: '开发文档', 4 | galgame: 'Galgame', 5 | kun: '关于鲲', 6 | notice: '公告' 7 | } 8 | -------------------------------------------------------------------------------- /constants/admin.ts: -------------------------------------------------------------------------------- 1 | import type { OverviewData } from '~/types/api/admin' 2 | 3 | export const APPLICANT_STATUS_MAP: Record = { 4 | 0: '待处理', 5 | 1: '已处理', 6 | 2: '已同意', 7 | 3: '已拒绝' 8 | } 9 | 10 | export const ADMIN_LOG_TYPE_MAP: Record = { 11 | create: '创建', 12 | delete: '删除', 13 | approve: '同意', 14 | decline: '拒绝', 15 | update: '更改' 16 | } 17 | 18 | export const ADMIN_STATS_SUM_MAP: Record = { 19 | userCount: '用户总数', 20 | galgameCount: 'Galgame 总数', 21 | patchResourceCount: 'Galgame 补丁总数', 22 | patchCommentCount: '评论总数' 23 | } 24 | 25 | export const ADMIN_STATS_MAP: Record = { 26 | newUser: '新注册用户', 27 | newActiveUser: '新活跃用户', 28 | newGalgame: '新发布 Galgame', 29 | newPatchResource: '新发布补丁', 30 | newComment: '新发布评论' 31 | } 32 | -------------------------------------------------------------------------------- /constants/api/select.ts: -------------------------------------------------------------------------------- 1 | export const GalgameCardSelectField = { 2 | id: true, 3 | name: true, 4 | banner: true, 5 | view: true, 6 | download: true, 7 | type: true, 8 | language: true, 9 | platform: true, 10 | content_limit: true, 11 | created: true, 12 | _count: { 13 | select: { 14 | favorite_by: true, 15 | contribute_by: true, 16 | resource: true, 17 | comment: true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /constants/captcha.ts: -------------------------------------------------------------------------------- 1 | export const kunCaptchaErrorMessageMap: Record = { 2 | 1: '杂鱼 ~ 连白毛老婆都分辨不出来了吗', 3 | 2: '杂鱼 ~ 怎么又选错了!', 4 | 3: '杂鱼 ~ 八嘎 ~ 杂鱼 ~ 臭杂鱼 ~', 5 | 4: '臭杂鱼 ~ 做不对验证的臭杂鱼 ~', 6 | 5: '八嘎八嘎八嘎八嘎八嘎八嘎八嘎八嘎', 7 | 6: '杂鱼 ~ 你不会是机器人吧' 8 | } 9 | -------------------------------------------------------------------------------- /constants/company.ts: -------------------------------------------------------------------------------- 1 | export const LOGO_KEY = 'kun-patch-company-logo' 2 | -------------------------------------------------------------------------------- /constants/galgame.ts: -------------------------------------------------------------------------------- 1 | export const GALGAME_AGE_LIMIT_MAP: Record = { 2 | sfw: 'SFW', 3 | nsfw: 'NSFW' 4 | } 5 | 6 | export const GALGAME_AGE_LIMIT_DETAIL: Record = { 7 | sfw: '本文章内容安全, 无 R18 等内容, 适合在公共场所浏览', 8 | nsfw: '本文章可能包含 R18 等内容, 不适合在公共场所浏览' 9 | } 10 | 11 | export const GALGAME_SORT_FIELD_LABEL_MAP: Record = { 12 | resource_update_time: '补丁更新时间', 13 | created: '游戏创建时间', 14 | view: '浏览量', 15 | download: '下载量' 16 | } 17 | 18 | const currentYear = new Date().getFullYear() 19 | export const GALGAME_SORT_YEARS = [ 20 | 'all', 21 | 'future', 22 | 'unknown', 23 | ...Array.from({ length: currentYear - 1979 }, (_, i) => 24 | String(currentYear - i) 25 | ) 26 | ] 27 | 28 | export const GALGAME_SORT_YEARS_MAP: Record = { 29 | all: '全部年份', 30 | future: '未发售', 31 | unknown: '未知年份' 32 | } 33 | 34 | export const GALGAME_SORT_MONTHS = [ 35 | 'all', 36 | '01', 37 | '02', 38 | '03', 39 | '04', 40 | '05', 41 | '06', 42 | '07', 43 | '08', 44 | '09', 45 | '10', 46 | '11', 47 | '12' 48 | ] 49 | -------------------------------------------------------------------------------- /constants/history.ts: -------------------------------------------------------------------------------- 1 | export const HISTORY_ACTION_TYPE = [ 2 | 'create', 3 | 'update', 4 | 'delete', 5 | 'merge', 6 | 'decline' 7 | ] as const 8 | 9 | export const HISTORY_ACTION_TYPE_MAP: Record = { 10 | create: '创建了', 11 | update: '更新了', 12 | delete: '删除了', 13 | merge: '合并了', 14 | decline: '拒绝了' 15 | } 16 | 17 | export const HISTORY_TYPE = [ 18 | 'galgame', 19 | 'introduction', 20 | 'tag', 21 | 'pr', 22 | 'banner' 23 | ] as const 24 | 25 | export const HISTORY_TYPE_MAP: Record = { 26 | galgame: 'Galgame', 27 | tag: 'Galgame 标签', 28 | pr: '更新请求', 29 | banner: '预览图' 30 | } 31 | -------------------------------------------------------------------------------- /constants/home-typed-js.ts: -------------------------------------------------------------------------------- 1 | export const kunTypedStrings = [ 2 | '开源的 Galgame 补丁资源下载站', 3 | '免费的 Galgame 补丁资源下载站', 4 | '零门槛的 Galgame 补丁资源下载站', 5 | '纯手写的 Galgame 补丁资源下载站', 6 | '最先进的 Galgame 补丁资源下载站' 7 | ] 8 | -------------------------------------------------------------------------------- /constants/message.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGE_TYPE = [ 2 | 'apply', 3 | 'pm', 4 | 'likeResource', 5 | 'likeComment', 6 | 'favorite', 7 | 'comment', 8 | 'follow', 9 | 'pr', 10 | 'mention', 11 | 'patchResourceCreate', 12 | 'patchResourceUpdate', 13 | 'system', 14 | '' 15 | ] as const 16 | 17 | export const MESSAGE_TYPE_MAP: Record = { 18 | apply: '申请', 19 | pm: '私聊', 20 | likeResource: '点赞资源', 21 | likeComment: '点赞评论', 22 | favorite: '收藏', 23 | comment: '评论', 24 | follow: '关注', 25 | pr: '更新请求', 26 | mention: '提到了您', 27 | patchResourceCreate: '创建新补丁', 28 | patchResourceUpdate: '更新补丁', 29 | system: '系统' 30 | } 31 | -------------------------------------------------------------------------------- /constants/routes/matcher.ts: -------------------------------------------------------------------------------- 1 | export const isPatchPath = (pathname: string): boolean => { 2 | return /^\/patch\/\d+/.test(pathname) 3 | } 4 | 5 | export const isTagPath = (pathname: string): boolean => { 6 | return /^\/tag\/\d+/.test(pathname) 7 | } 8 | 9 | export const isCompanyPath = (pathname: string): boolean => { 10 | return /^\/company\/\d+/.test(pathname) 11 | } 12 | 13 | export const isUserPath = (pathname: string): boolean => { 14 | return /^\/user\/\d+/.test(pathname) 15 | } 16 | -------------------------------------------------------------------------------- /constants/top-bar.ts: -------------------------------------------------------------------------------- 1 | export interface KunNavItem { 2 | name: string 3 | href: string 4 | } 5 | 6 | export const kunNavItem: KunNavItem[] = [ 7 | { 8 | name: '下载', 9 | href: '/galgame' 10 | }, 11 | { 12 | name: '发布', 13 | href: '/edit/create' 14 | }, 15 | { 16 | name: '标签', 17 | href: '/tag' 18 | }, 19 | { 20 | name: '会社', 21 | href: '/company' 22 | }, 23 | { 24 | name: '新作', 25 | href: '/release' 26 | }, 27 | { 28 | name: '排行', 29 | href: '/ranking/user' 30 | }, 31 | { 32 | name: '关于', 33 | href: '/about' 34 | } 35 | ] 36 | 37 | export const kunMobileNavItem: KunNavItem[] = [ 38 | ...kunNavItem, 39 | { 40 | name: '补丁评论列表', 41 | href: '/comment' 42 | }, 43 | { 44 | name: '补丁资源列表', 45 | href: '/resource' 46 | }, 47 | { 48 | name: '管理系统', 49 | href: '/admin' 50 | }, 51 | { 52 | name: '联系我们', 53 | href: '/about/notice/feedback' 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /constants/user.ts: -------------------------------------------------------------------------------- 1 | export const USER_ROLE_MAP: Record = { 2 | 1: '用户', 3 | 2: '创作者', 4 | 3: '管理员', 5 | 4: '超级管理员' 6 | } 7 | 8 | export const USER_STATUS_MAP: Record = { 9 | 0: '正常', 10 | 1: '限制(正在开发中)', 11 | 2: '封禁' 12 | } 13 | 14 | export const USER_STATUS_COLOR_MAP: Record< 15 | number, 16 | 'success' | 'warning' | 'danger' 17 | > = { 18 | 0: 'success', 19 | 1: 'warning', 20 | 2: 'danger' 21 | } 22 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | apps: [ 5 | { 6 | name: 'kun-visual-novel-patch', 7 | port: 2333, 8 | cwd: path.join(__dirname), 9 | instances: 1, 10 | autorestart: true, 11 | watch: false, 12 | max_memory_restart: '1G', 13 | script: './.next/standalone/server.js' 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useMounted = () => { 4 | const [mounted, setMounted] = useState(false) 5 | 6 | useEffect(() => { 7 | setMounted(true) 8 | }, []) 9 | 10 | return mounted 11 | } 12 | -------------------------------------------------------------------------------- /hooks/useResizeObserver.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useCallback, useEffect, useState } from 'react' 4 | 5 | interface Size { 6 | width: number | undefined 7 | height: number | undefined 8 | } 9 | 10 | export const useResizeObserver = (ref: React.RefObject) => { 11 | const [size, setSize] = useState({ 12 | width: undefined, 13 | height: undefined 14 | }) 15 | 16 | const handleResize = useCallback((entries: ResizeObserverEntry[]) => { 17 | const entry = entries[0] 18 | 19 | if (entry) { 20 | const { width, height } = entry.contentRect 21 | setSize({ width, height }) 22 | } 23 | }, []) 24 | 25 | useEffect(() => { 26 | if (!ref.current) { 27 | return 28 | } 29 | 30 | const observer = new ResizeObserver(handleResize) 31 | observer.observe(ref.current) 32 | 33 | return () => { 34 | observer.disconnect() 35 | } 36 | }, [ref, handleResize]) 37 | 38 | return size 39 | } 40 | -------------------------------------------------------------------------------- /hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState, useEffect } from 'react' 4 | 5 | interface WindowSize { 6 | width: number 7 | height: number 8 | } 9 | 10 | export const useWindowSize = (): WindowSize => { 11 | const [windowSize, setWindowSize] = useState({ 12 | width: 0, 13 | height: 0 14 | }) 15 | 16 | useEffect(() => { 17 | function handleResize() { 18 | setWindowSize({ 19 | width: window.innerWidth, 20 | height: window.innerHeight 21 | }) 22 | } 23 | 24 | window.addEventListener('resize', handleResize) 25 | 26 | handleResize() 27 | 28 | return () => window.removeEventListener('resize', handleResize) 29 | }, []) 30 | 31 | return windowSize 32 | } 33 | -------------------------------------------------------------------------------- /instrumentation.ts: -------------------------------------------------------------------------------- 1 | export const register = async () => { 2 | if (process.env.NEXT_RUNTIME === 'nodejs') { 3 | const { setKUNGalgameTask } = await import('~/server/cron') 4 | setKUNGalgameTask() 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/mdx/CustomMDX.tsx: -------------------------------------------------------------------------------- 1 | import { MDXRemote, MDXRemoteProps } from 'next-mdx-remote/rsc' 2 | import { KunLink } from './element/KunLink' 3 | import { KunTable } from './element/KunTable' 4 | import { KunCode } from './element/KunCode' 5 | import { createKunHeading } from './element/kunHeading' 6 | 7 | const components = { 8 | h1: createKunHeading(1), 9 | h2: createKunHeading(2), 10 | h3: createKunHeading(3), 11 | h4: createKunHeading(4), 12 | h5: createKunHeading(5), 13 | h6: createKunHeading(6), 14 | a: KunLink, 15 | code: KunCode, 16 | Table: KunTable 17 | } 18 | 19 | export const CustomMDX = (props: MDXRemoteProps) => { 20 | return ( 21 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /lib/mdx/element/KunCode.tsx: -------------------------------------------------------------------------------- 1 | import { highlight } from 'sugar-high' 2 | import React, { FC } from 'react' 3 | 4 | interface CodeProps extends React.HTMLAttributes { 5 | children: string 6 | } 7 | 8 | export const KunCode: FC = ({ children, ...props }) => { 9 | const codeHTML = highlight(children) 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /lib/mdx/element/KunLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React, { FC } from 'react' 3 | 4 | interface CustomLinkProps 5 | extends React.AnchorHTMLAttributes { 6 | href: string 7 | } 8 | 9 | export const KunLink: FC = ({ href, children, ...props }) => { 10 | if (href.startsWith('/')) { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | if (href.startsWith('#')) { 19 | return ( 20 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /lib/mdx/element/KunTable.tsx: -------------------------------------------------------------------------------- 1 | interface TableProps { 2 | data: { 3 | headers: string[] 4 | rows: string[][] 5 | } 6 | } 7 | 8 | export const KunTable = ({ data }: TableProps) => { 9 | const headers = data.headers.map((header, index) => ( 10 | {header} 11 | )) 12 | const rows = data.rows.map((row, index) => ( 13 | 14 | {row.map((cell, cellIndex) => ( 15 | {cell} 16 | ))} 17 | 18 | )) 19 | 20 | return ( 21 | 22 | 23 | {headers} 24 | 25 | {rows} 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /lib/mdx/element/kunHeading.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | const slugify = (str: string): string => { 4 | return str 5 | .toString() 6 | .toLowerCase() 7 | .trim() 8 | .replace(/\s+/g, '-') 9 | .replace(/&/g, '-and-') 10 | .replace(/[^\p{L}\p{N}]+/gu, '') 11 | .replace(/--+/g, '-') 12 | .replace(/^-+|-+$/g, '') 13 | } 14 | 15 | export const createKunHeading = (level: number) => { 16 | const Heading = ({ children }: { children: ReactNode }) => { 17 | const slug = slugify(children?.toString() || '') 18 | return React.createElement( 19 | `h${level}`, 20 | { id: slug }, 21 | [ 22 | React.createElement('a', { 23 | href: `#${slug}`, 24 | key: `kun-link-${slug}`, 25 | className: 'kun-anchor', 26 | 'aria-label': slug 27 | }) 28 | ], 29 | children 30 | ) 31 | } 32 | 33 | Heading.displayName = `KunHeading${level}` 34 | 35 | return Heading 36 | } 37 | -------------------------------------------------------------------------------- /lib/mdx/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface KunPostMetadata { 2 | title: string 3 | banner: string 4 | date: string 5 | description: string 6 | textCount: number 7 | slug: string 8 | path: string 9 | } 10 | 11 | export interface KunTreeNode { 12 | name: string 13 | label: string 14 | path: string 15 | children?: KunTreeNode[] 16 | type: 'file' | 'directory' 17 | } 18 | 19 | export interface KunFrontmatter { 20 | title: string 21 | banner: string 22 | description: string 23 | date: string 24 | authorUid: number 25 | authorName: string 26 | authorAvatar: string 27 | authorHomepage: string 28 | } 29 | 30 | export interface KunBlog { 31 | slug: string 32 | content: string 33 | frontmatter: KunFrontmatter 34 | } 35 | -------------------------------------------------------------------------------- /lib/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis' 2 | 3 | const KUN_PATCH_REDIS_PREFIX = 'kun:patch' 4 | 5 | export const redis = new Redis({ 6 | port: parseInt(process.env.REDIS_PORT!), 7 | host: process.env.REDIS_HOST 8 | }) 9 | 10 | export const setKv = async (key: string, value: string, time?: number) => { 11 | const keyString = `${KUN_PATCH_REDIS_PREFIX}:${key}` 12 | if (time) { 13 | await redis.setex(keyString, time, value) 14 | } else { 15 | await redis.set(keyString, value) 16 | } 17 | } 18 | 19 | export const getKv = async (key: string) => { 20 | const keyString = `${KUN_PATCH_REDIS_PREFIX}:${key}` 21 | const value = await redis.get(keyString) 22 | return value 23 | } 24 | 25 | export const delKv = async (key: string) => { 26 | const keyString = `${KUN_PATCH_REDIS_PREFIX}:${key}` 27 | await redis.del(keyString) 28 | } 29 | -------------------------------------------------------------------------------- /lib/s3/client.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from '@aws-sdk/client-s3' 2 | 3 | export const s3 = new S3Client({ 4 | endpoint: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_ENDPOINT!, 5 | region: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_REGION!, 6 | credentials: { 7 | accessKeyId: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_ACCESS_KEY_ID!, 8 | secretAccessKey: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_SECRET_ACCESS_KEY! 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /lib/s3/deleteFileFromS3.ts: -------------------------------------------------------------------------------- 1 | import { s3 } from './client' 2 | import { DeleteObjectCommand } from '@aws-sdk/client-s3' 3 | 4 | export const deleteFileFromS3 = async (key: string) => { 5 | const deleteCommand = new DeleteObjectCommand({ 6 | Bucket: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_BUCKET_NAME!, 7 | Key: key 8 | }) 9 | await s3.send(deleteCommand) 10 | } 11 | -------------------------------------------------------------------------------- /lib/s3/uploadImageToS3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3' 2 | 3 | // Image will put to a different storage provider 4 | export const s3 = new S3Client({ 5 | endpoint: process.env.KUN_VISUAL_NOVEL_IMAGE_BED_ENDPOINT!, 6 | region: 'auto', 7 | credentials: { 8 | accessKeyId: process.env.KUN_VISUAL_NOVEL_IMAGE_BED_ACCESS_KEY!, 9 | secretAccessKey: process.env.KUN_VISUAL_NOVEL_IMAGE_BED_SECRET_KEY! 10 | } 11 | }) 12 | 13 | export const uploadImageToS3 = async (key: string, fileBuffer: Buffer) => { 14 | const uploadCommand = new PutObjectCommand({ 15 | Bucket: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_BUCKET_NAME!, 16 | Key: key, 17 | Body: fileBuffer, 18 | ContentType: 'application/octet-stream' 19 | }) 20 | await s3.send(uploadCommand) 21 | } 22 | -------------------------------------------------------------------------------- /lib/s3/uploadSmallFileToS3.ts: -------------------------------------------------------------------------------- 1 | import { s3 } from './client' 2 | import { readFile } from 'fs/promises' 3 | import { PutObjectCommand } from '@aws-sdk/client-s3' 4 | 5 | export const uploadSmallFileToS3 = async (key: string, filePath: string) => { 6 | try { 7 | const fileBuffer = await readFile(filePath) 8 | const uploadCommand = new PutObjectCommand({ 9 | Bucket: process.env.KUN_VISUAL_NOVEL_S3_STORAGE_BUCKET_NAME!, 10 | Key: key, 11 | Body: fileBuffer, 12 | ContentType: 'application/octet-stream' 13 | }) 14 | await s3.send(uploadCommand) 15 | } catch (error) { 16 | return '上传文件错误, uploadSmallFileToS3 function ERROR' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /middleware/_verifyHeaderCookie.ts: -------------------------------------------------------------------------------- 1 | import { parseCookies } from '~/utils/cookies' 2 | import { verifyKunToken } from '~/app/api/utils/jwt' 3 | import type { NextRequest } from 'next/server' 4 | 5 | export const verifyHeaderCookie = async (req: NextRequest) => { 6 | const token = parseCookies(req.headers.get('cookie') ?? '')[ 7 | 'kun-galgame-patch-moe-token' 8 | ] 9 | const payload = await verifyKunToken(token ?? '') 10 | 11 | return payload 12 | } 13 | -------------------------------------------------------------------------------- /middleware/middleware.ts: -------------------------------------------------------------------------------- 1 | // import { NextResponse } from 'next/server' 2 | // import type { NextRequest } from 'next/server' 3 | 4 | // export async function middleware(request: NextRequest) { 5 | // const { kunAuthMiddleware } = await import('./auth') 6 | // kunAuthMiddleware(request) 7 | // return NextResponse.next() 8 | // } 9 | -------------------------------------------------------------------------------- /migration/migrateAlias.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import { readFile } from 'fs/promises' 3 | import path from 'path' 4 | import { fileURLToPath } from 'url' 5 | 6 | const __filename = fileURLToPath(import.meta.url) 7 | const __dirname = path.dirname(__filename) 8 | 9 | const prisma = new PrismaClient() 10 | 11 | async function main() { 12 | try { 13 | const filePath = `${__dirname}/patches.json` 14 | const data = await readFile(filePath, 'utf-8') 15 | const patches = JSON.parse(data) 16 | 17 | console.log(`开始迁移 ${patches.length} 条 alias 数据...`) 18 | 19 | for (const { id, alias } of patches) { 20 | await prisma.patch_alias.create({ 21 | data: { 22 | name: alias, 23 | patch: { 24 | connect: { id: id } 25 | } 26 | } 27 | }) 28 | } 29 | 30 | console.log('数据迁移完成!') 31 | } catch (error) { 32 | console.error('数据迁移失败:', error) 33 | } finally { 34 | await prisma.$disconnect() 35 | } 36 | } 37 | 38 | main() 39 | -------------------------------------------------------------------------------- /migration/updatePatchResourceUpdateTime.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const prisma = new PrismaClient() 4 | 5 | const updatePatchResourceUpdateTime = async () => { 6 | try { 7 | const patches = await prisma.patch.findMany({ 8 | select: { 9 | id: true, 10 | created: true 11 | } 12 | }) 13 | 14 | for (const patch of patches) { 15 | await prisma.patch.update({ 16 | where: { id: patch.id }, 17 | data: { 18 | resource_update_time: patch.created 19 | } 20 | }) 21 | 22 | console.log(`Updated patch with id: ${patch.id}`) 23 | } 24 | 25 | console.log('Successfully updated all patch records.') 26 | } catch (error) { 27 | console.error('Error updating patch records:', error) 28 | } finally { 29 | await prisma.$disconnect() 30 | } 31 | } 32 | 33 | updatePatchResourceUpdateTime() 34 | -------------------------------------------------------------------------------- /migration/updateReleased.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const prisma = new PrismaClient() 4 | 5 | async function updateReleasedField() { 6 | try { 7 | const updatedRecords = await prisma.patch.updateMany({ 8 | where: { 9 | released: '' 10 | }, 11 | data: { 12 | released: 'unknown' 13 | } 14 | }) 15 | 16 | console.log(`Updated ${updatedRecords.count} records.`) 17 | } catch (error) { 18 | console.error('Error updating records: ', error) 19 | } finally { 20 | await prisma.$disconnect() 21 | } 22 | } 23 | 24 | updateReleasedField() 25 | -------------------------------------------------------------------------------- /migration/updateResourceUpdateTime.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const prisma = new PrismaClient() 4 | 5 | const updateResourceUpdateTime = async () => { 6 | try { 7 | const resources = await prisma.patch_resource.findMany({ 8 | select: { 9 | id: true, 10 | created: true 11 | } 12 | }) 13 | 14 | for (const resource of resources) { 15 | await prisma.patch_resource.update({ 16 | where: { id: resource.id }, 17 | data: { 18 | update_time: resource.created 19 | } 20 | }) 21 | 22 | console.log(`Updated resource with id: ${resource.id}`) 23 | } 24 | 25 | console.log('Successfully updated all resource records.') 26 | } catch (error) { 27 | console.error('Error updating resource records:', error) 28 | } finally { 29 | await prisma.$disconnect() 30 | } 31 | } 32 | 33 | updateResourceUpdateTime() 34 | -------------------------------------------------------------------------------- /migration/userDailyUploadSize.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const prisma = new PrismaClient() 4 | 5 | async function migrateDailyUploadSize() { 6 | try { 7 | console.log('Starting migration...') 8 | 9 | // Step 1: 更新所有用户的数据,将 MB 转换为 Byte(避免精度丢失) 10 | await prisma.$executeRaw`UPDATE "user" SET "daily_upload_size" = "daily_upload_size" * 1024 * 1024;` 11 | 12 | console.log('Data conversion completed.') 13 | 14 | // Step 2: 修改数据库字段类型 15 | await prisma.$executeRaw`ALTER TABLE "user" ALTER COLUMN "daily_upload_size" SET DATA TYPE INT;` 16 | 17 | console.log('Column type updated to INT.') 18 | } catch (error) { 19 | console.error('Migration failed:', error) 20 | } finally { 21 | await prisma.$disconnect() 22 | } 23 | } 24 | 25 | // 执行迁移 26 | migrateDailyUploadSize() 27 | -------------------------------------------------------------------------------- /motion/bell.ts: -------------------------------------------------------------------------------- 1 | export const dotVariants = { 2 | initial: { scale: 0, opacity: 0 }, 3 | animate: { scale: 1, opacity: 1 }, 4 | exit: { scale: 0, opacity: 0 } 5 | } 6 | 7 | export const bellShakeVariants = { 8 | initial: { rotate: 0 }, 9 | animate: { 10 | rotate: [0, -10, 10, -10, 10, 0], 11 | transition: { 12 | duration: 0.5, 13 | repeat: Infinity, 14 | repeatDelay: 2 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /motion/card.ts: -------------------------------------------------------------------------------- 1 | export const cardContainer = { 2 | hidden: { opacity: 0 }, 3 | show: { 4 | opacity: 1, 5 | transition: { 6 | staggerChildren: 0.1 7 | } 8 | } 9 | } 10 | 11 | export const cardItem = { 12 | hidden: { opacity: 0, y: 20 }, 13 | show: { opacity: 1, y: 0 } 14 | } 15 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const globalForPrisma = global as unknown as { prisma: PrismaClient } 4 | 5 | export const prisma = globalForPrisma.prisma ?? new PrismaClient() 6 | 7 | if (process.env.NODE_ENV !== 'production') { 8 | globalForPrisma.prisma = prisma 9 | } 10 | -------------------------------------------------------------------------------- /public/apple-touch-icon.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/apple-touch-icon.avif -------------------------------------------------------------------------------- /public/edit/1.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/edit/1.avif -------------------------------------------------------------------------------- /public/edit/2.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/edit/2.avif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/favicon.webp -------------------------------------------------------------------------------- /public/kungalgame.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/kungalgame.avif -------------------------------------------------------------------------------- /public/placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/placeholder.webp -------------------------------------------------------------------------------- /public/posts/dev/documentation/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/dev/documentation/banner.avif -------------------------------------------------------------------------------- /public/posts/galgame/resource/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/galgame/resource/banner.avif -------------------------------------------------------------------------------- /public/posts/kun/moe/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/kun/moe/banner.avif -------------------------------------------------------------------------------- /public/posts/kun/moe/kun.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/kun/moe/kun.webp -------------------------------------------------------------------------------- /public/posts/kun/ren/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/kun/ren/banner.avif -------------------------------------------------------------------------------- /public/posts/kun/ren/ren1.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/kun/ren/ren1.avif -------------------------------------------------------------------------------- /public/posts/kun/ren/ren2.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/kun/ren/ren2.avif -------------------------------------------------------------------------------- /public/posts/notice/about/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/about/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/cfmsc/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/cfmsc/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/creator/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/creator/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/feedback/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/feedback/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/galgame-tutorial/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/galgame-tutorial/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/moemoepoint/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/moemoepoint/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/nsfw/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/nsfw/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/open-source/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/open-source/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/paradigm/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/paradigm/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image1.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image1.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image2.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image2.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image3.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image3.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image4.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image4.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image5.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image5.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image6.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image6.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image7.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image7.avif -------------------------------------------------------------------------------- /public/posts/notice/patch-tutorial/image8.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/patch-tutorial/image8.avif -------------------------------------------------------------------------------- /public/posts/notice/privacy/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/privacy/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/rule/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/rule/banner.avif -------------------------------------------------------------------------------- /public/posts/notice/update/banner.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/posts/notice/update/banner.avif -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /message 4 | -------------------------------------------------------------------------------- /public/sooner/あーちゃん.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/sooner/あーちゃん.webp -------------------------------------------------------------------------------- /public/sooner/こじかひわ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/sooner/こじかひわ.webp -------------------------------------------------------------------------------- /public/sooner/琥珀.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/sooner/琥珀.webp -------------------------------------------------------------------------------- /public/sooner/雪々.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/public/sooner/雪々.webp -------------------------------------------------------------------------------- /scripts/deployBuild.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import { config } from 'dotenv' 3 | import { envSchema } from '../validations/dotenv-check' 4 | import * as fs from 'fs' 5 | import * as path from 'path' 6 | 7 | const envPath = path.resolve(__dirname, '..', '.env') 8 | if (!fs.existsSync(envPath)) { 9 | console.error('.env file not found in the project root.') 10 | process.exit(1) 11 | } 12 | 13 | config({ path: envPath }) 14 | 15 | try { 16 | envSchema.safeParse(process.env) 17 | 18 | console.log('Environment variables are valid.') 19 | console.log('Executing the commands...') 20 | 21 | execSync( 22 | 'git pull && pnpm prisma:push && pnpm build && pnpm stop && pnpm start', 23 | { stdio: 'inherit' } 24 | ) 25 | } catch (error) { 26 | console.error('Invalid environment variables', error) 27 | process.exit(1) 28 | } 29 | -------------------------------------------------------------------------------- /scripts/deployInstall.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | 3 | const runCommand = (command: string) => { 4 | try { 5 | console.log(`Running command: ${command}`) 6 | execSync(command, { stdio: 'inherit' }) 7 | } catch (error) { 8 | console.error(`Error running command: ${command}`, error) 9 | process.exit(1) 10 | } 11 | } 12 | 13 | runCommand('pnpm install') 14 | 15 | runCommand('pnpx prisma:push') 16 | -------------------------------------------------------------------------------- /server/cron.ts: -------------------------------------------------------------------------------- 1 | import { resetDailyTask } from './tasks/resetDailyTask' 2 | import { setCleanupTask } from './tasks/setCleanupTask' 3 | 4 | export const setKUNGalgameTask = () => { 5 | resetDailyTask 6 | setCleanupTask 7 | } 8 | -------------------------------------------------------------------------------- /server/image/auth/other/22.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/22.webp -------------------------------------------------------------------------------- /server/image/auth/other/24.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/24.webp -------------------------------------------------------------------------------- /server/image/auth/other/29.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/29.webp -------------------------------------------------------------------------------- /server/image/auth/other/37.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/37.webp -------------------------------------------------------------------------------- /server/image/auth/other/38.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/38.webp -------------------------------------------------------------------------------- /server/image/auth/other/46.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/46.webp -------------------------------------------------------------------------------- /server/image/auth/other/50.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/50.webp -------------------------------------------------------------------------------- /server/image/auth/other/54.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/54.webp -------------------------------------------------------------------------------- /server/image/auth/other/59.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/59.webp -------------------------------------------------------------------------------- /server/image/auth/other/64.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/64.webp -------------------------------------------------------------------------------- /server/image/auth/other/65.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/65.webp -------------------------------------------------------------------------------- /server/image/auth/other/70.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/70.webp -------------------------------------------------------------------------------- /server/image/auth/other/71.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/other/71.webp -------------------------------------------------------------------------------- /server/image/auth/white/14.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/14.webp -------------------------------------------------------------------------------- /server/image/auth/white/16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/16.webp -------------------------------------------------------------------------------- /server/image/auth/white/26.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/26.webp -------------------------------------------------------------------------------- /server/image/auth/white/51.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/51.webp -------------------------------------------------------------------------------- /server/image/auth/white/59.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/59.webp -------------------------------------------------------------------------------- /server/image/auth/white/6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/6.webp -------------------------------------------------------------------------------- /server/image/auth/white/7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/7.webp -------------------------------------------------------------------------------- /server/image/auth/white/76.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/76.webp -------------------------------------------------------------------------------- /server/image/auth/white/8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/8.webp -------------------------------------------------------------------------------- /server/image/auth/white/9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KUN1007/kun-galgame-patch-next/5edcf363d5120951016d376e5213b8b3affd542d/server/image/auth/white/9.webp -------------------------------------------------------------------------------- /server/tasks/resetDailyTask.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '~/prisma' 2 | import cron from 'node-cron' 3 | 4 | export const resetDailyTask = cron.schedule('0 0 * * *', async () => { 5 | await prisma.user.updateMany({ 6 | data: { 7 | daily_image_count: 0, 8 | daily_check_in: 0, 9 | daily_upload_size: 0 10 | } 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /store/milkdownStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | export interface MilkdownData { 4 | refreshContentStatus: boolean 5 | } 6 | 7 | interface StoreState { 8 | data: MilkdownData 9 | refreshMilkdownContent: () => void 10 | } 11 | 12 | const initialState: MilkdownData = { 13 | refreshContentStatus: false 14 | } 15 | 16 | export const useKunMilkdownStore = create()((set, get) => ({ 17 | data: initialState, 18 | refreshMilkdownContent: () => { 19 | const currentStatus = get().data.refreshContentStatus 20 | set({ data: { refreshContentStatus: !currentStatus } }) 21 | } 22 | })) 23 | -------------------------------------------------------------------------------- /store/rewriteStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | export interface RewritePatchData { 4 | id: number 5 | vndbId: string 6 | name: string 7 | introduction: string 8 | alias: string[] 9 | contentLimit: string 10 | released: string 11 | } 12 | 13 | interface StoreState { 14 | data: RewritePatchData 15 | getData: () => RewritePatchData 16 | setData: (data: RewritePatchData) => void 17 | resetData: () => void 18 | } 19 | 20 | const initialState: RewritePatchData = { 21 | id: 0, 22 | vndbId: '', 23 | name: '', 24 | introduction: '', 25 | alias: [], 26 | contentLimit: 'sfw', 27 | released: '' 28 | } 29 | 30 | export const useRewritePatchStore = create()((set, get) => ({ 31 | data: initialState, 32 | getData: () => get().data, 33 | setData: (data: RewritePatchData) => set({ data }), 34 | resetData: () => set({ data: initialState }) 35 | })) 36 | -------------------------------------------------------------------------------- /store/searchStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | 4 | export interface CreateSearchData { 5 | searchHistory: string[] 6 | searchInIntroduction: boolean 7 | searchInAlias: boolean 8 | searchInTag: boolean 9 | } 10 | 11 | const initialState: CreateSearchData = { 12 | searchHistory: [], 13 | searchInIntroduction: false, 14 | searchInAlias: true, 15 | searchInTag: false 16 | } 17 | 18 | interface SearchStoreState { 19 | data: CreateSearchData 20 | getData: () => CreateSearchData 21 | setData: (data: CreateSearchData) => void 22 | resetData: () => void 23 | } 24 | 25 | export const useSearchStore = create()( 26 | persist( 27 | (set, get) => ({ 28 | data: initialState, 29 | getData: () => get().data, 30 | setData: (data: CreateSearchData) => set({ data }), 31 | resetData: () => set({ data: initialState }) 32 | }), 33 | { 34 | name: 'kun-patch-search-store', 35 | storage: createJSONStorage(() => localStorage) 36 | } 37 | ) 38 | ) 39 | -------------------------------------------------------------------------------- /store/settingStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist, createJSONStorage } from 'zustand/middleware' 3 | import { cookieStorage } from './_cookie' 4 | 5 | export interface KunSettingData { 6 | kunNsfwEnable: string 7 | } 8 | 9 | interface StoreState { 10 | data: KunSettingData 11 | getData: () => KunSettingData 12 | setData: (data: KunSettingData) => void 13 | resetData: () => void 14 | } 15 | 16 | const initialState: KunSettingData = { 17 | kunNsfwEnable: 'sfw' 18 | } 19 | 20 | export const useSettingStore = create()( 21 | persist( 22 | (set, get) => ({ 23 | data: initialState, 24 | getData: () => get().data, 25 | setData: (data: KunSettingData) => set({ data }), 26 | resetData: () => set({ data: initialState }) 27 | }), 28 | { 29 | name: 'kun-patch-setting-store', 30 | storage: createJSONStorage(() => cookieStorage) 31 | } 32 | ) 33 | ) 34 | -------------------------------------------------------------------------------- /styles/blog.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --sh-class: hsl(var(--nextui-primary-500)); 3 | --sh-identifier: hsl(var(--nextui-default-800)); 4 | --sh-sign: hsl(var(--nextui-default-500)); 5 | --sh-property: hsl(var(--nextui-primary-700)); 6 | --sh-entity: hsl(var(--nextui-success-600)); 7 | --sh-jsxliterals: hsl(var(--nextui-secondary-600)); 8 | --sh-string: hsl(var(--nextui-success-500)); 9 | --sh-keyword: hsl(var(--nextui-warning-600)); 10 | --sh-comment: hsl(var(--nextui-default-400)); 11 | } 12 | -------------------------------------------------------------------------------- /styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './tailwind.scss'; 2 | @use './blog.scss'; 3 | @use './prose.scss'; 4 | 5 | [data-overlay-container='true'] { 6 | height: 100%; 7 | width: 100%; 8 | } 9 | 10 | /** TODO: **/ 11 | * { 12 | font-family: '微软雅黑'; 13 | } 14 | -------------------------------------------------------------------------------- /styles/tailwind.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | import { nextui } from '@nextui-org/react' 3 | import typography from '@tailwindcss/typography' 4 | 5 | /** @type {import('tailwindcss').Config} */ 6 | const config = { 7 | content: [ 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 10 | './components/**/*.{js,ts,jsx,tsx,mdx}', 11 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 12 | ], 13 | theme: { 14 | extend: {} 15 | }, 16 | darkMode: 'class', 17 | future: { 18 | hoverOnlyWhenSupported: true 19 | }, 20 | plugins: [nextui(), typography] 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["./*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /types/api/comment.d.ts: -------------------------------------------------------------------------------- 1 | export interface PatchComment { 2 | id: number 3 | user: KunUser 4 | content: string 5 | patchName: string 6 | patchId: number 7 | like: number 8 | created: Date | string 9 | } 10 | -------------------------------------------------------------------------------- /types/api/company.d.ts: -------------------------------------------------------------------------------- 1 | export interface Company { 2 | id: number 3 | name: string 4 | logo: string 5 | count: number 6 | alias: string[] 7 | } 8 | 9 | export interface CompanyDetail extends Company { 10 | introduction: string 11 | primary_language: string[] 12 | official_website: string[] 13 | parent_brand: string[] 14 | created: string | Date 15 | user: KunUser 16 | } 17 | -------------------------------------------------------------------------------- /types/api/galgame.d.ts: -------------------------------------------------------------------------------- 1 | interface GalgameCard { 2 | id: number 3 | name: string 4 | banner: string 5 | view: number 6 | download: number 7 | type: string[] 8 | language: string[] 9 | platform: string[] 10 | content_limit: string 11 | created: Date | string 12 | _count: { 13 | favorite_by: number 14 | contribute_by: number 15 | resource: number 16 | comment: number 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /types/api/home.d.ts: -------------------------------------------------------------------------------- 1 | import type { PatchComment } from './comment' 2 | import type { PatchResource } from './resource' 3 | 4 | export interface HomeCarousel { 5 | id: number 6 | galgameTitle: string 7 | description: string 8 | type: string[] 9 | language: string[] 10 | platform: string[] 11 | } 12 | 13 | export type HomeResource = PatchResource 14 | export type HomeComment = PatchComment 15 | -------------------------------------------------------------------------------- /types/api/message.d.ts: -------------------------------------------------------------------------------- 1 | import { MESSAGE_TYPE } from '~/constants/message' 2 | 3 | export interface Message { 4 | id: number 5 | type: string 6 | content: string 7 | status: number 8 | link: string 9 | created: string | Date 10 | sender: KunUser | null 11 | } 12 | 13 | export interface CreateMessageType { 14 | type: (typeof MESSAGE_TYPE)[number] 15 | content: string 16 | link: string 17 | sender_id?: number 18 | recipient_id?: number 19 | } 20 | -------------------------------------------------------------------------------- /types/api/ranking.d.ts: -------------------------------------------------------------------------------- 1 | export type RankingUser = { 2 | id: number 3 | name: string 4 | avatar: string 5 | moemoepoint: number 6 | patchCount: number 7 | resourceCount: number 8 | commentCount: number 9 | } 10 | -------------------------------------------------------------------------------- /types/api/release.d.ts: -------------------------------------------------------------------------------- 1 | export interface GalgameReleaseCard { 2 | patchId: number 3 | name: string 4 | banner: string 5 | released: string 6 | resourceCount: number 7 | } 8 | -------------------------------------------------------------------------------- /types/api/resource.d.ts: -------------------------------------------------------------------------------- 1 | export interface PatchResource { 2 | id: number 3 | storage: string 4 | name: string 5 | modelName: string 6 | size: string 7 | type: string[] 8 | language: string[] 9 | platform: string[] 10 | note: string 11 | likeCount: number 12 | download: number 13 | patchId: number 14 | patchName: string 15 | created: string 16 | user: KunUser & { 17 | patchCount: number 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /types/api/tag.d.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | id: number 3 | name: string 4 | count: number 5 | alias: string[] 6 | } 7 | 8 | export interface TagDetail extends Tag { 9 | introduction: string 10 | created: string | Date 11 | user: KunUser 12 | } 13 | -------------------------------------------------------------------------------- /types/api/upload.d.ts: -------------------------------------------------------------------------------- 1 | import { SUPPORTED_RESOURCE_LINK } from '~/constants/resource' 2 | 3 | export interface UploadFileResponse { 4 | filetype: (typeof SUPPORTED_RESOURCE_LINK)[number] 5 | fileHash: string 6 | fileSize: string 7 | } 8 | 9 | export interface KunChunkMetadata { 10 | chunkIndex: number 11 | totalChunks: number 12 | fileId: string 13 | fileName: string 14 | fileSize: number 15 | mimeType: string 16 | filepath: string 17 | fileHash: string 18 | } 19 | -------------------------------------------------------------------------------- /types/response.d.ts: -------------------------------------------------------------------------------- 1 | type KunResponse = string | T 2 | -------------------------------------------------------------------------------- /types/user.d.ts: -------------------------------------------------------------------------------- 1 | interface KunUser { 2 | id: number 3 | name: string 4 | avatar: string 5 | } 6 | -------------------------------------------------------------------------------- /utils/actions/getNSFWHeader.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { cookies } from 'next/headers' 4 | 5 | export const getNSFWHeader = async () => { 6 | const cookieStore = await cookies() 7 | const token = cookieStore.get( 8 | 'kun-patch-setting-store|state|data|kunNsfwEnable' 9 | )?.value 10 | 11 | if (!token) { 12 | return { content_limit: 'sfw' } 13 | } 14 | 15 | if (token === 'all') { 16 | return {} 17 | } else { 18 | return { content_limit: token } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utils/actions/safeParseSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { ZodSchema } from 'zod' 3 | 4 | export const safeParseSchema = ( 5 | schema: T, 6 | object: Record 7 | ): z.infer | string => { 8 | const result = schema.safeParse(object) 9 | if (!result.success) { 10 | return result.error.message 11 | } 12 | return result.data 13 | } 14 | -------------------------------------------------------------------------------- /utils/actions/verifyHeaderCookie.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { cookies } from 'next/headers' 4 | import { verifyKunToken } from '~/app/api/utils/jwt' 5 | 6 | export const verifyHeaderCookie = async () => { 7 | const cookieStore = await cookies() 8 | const token = cookieStore.get('kun-galgame-patch-moe-token') 9 | const payload = await verifyKunToken(token?.value ?? '') 10 | 11 | return payload 12 | } 13 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /utils/cookies.ts: -------------------------------------------------------------------------------- 1 | export const parseCookies = (cookieString: string) => { 2 | const cookiesKv: { [key: string]: string } = {} 3 | cookieString && 4 | cookieString.split(';').forEach((cookie) => { 5 | const parts: string[] = cookie.split('=') 6 | if (parts.length) { 7 | cookiesKv[parts.shift()!.trim()] = decodeURI(parts.join('=')) 8 | } 9 | }) 10 | return cookiesKv 11 | } 12 | -------------------------------------------------------------------------------- /utils/dataURItoBlob.ts: -------------------------------------------------------------------------------- 1 | export const dataURItoBlob = (dataURI: string) => { 2 | const byteString = atob(dataURI.split(',')[1]) 3 | const ab = new ArrayBuffer(byteString.length) 4 | const ia = new Uint8Array(ab) 5 | for (let i = 0; i < byteString.length; i++) { 6 | ia[i] = byteString.charCodeAt(i) 7 | } 8 | return new Blob([ab], { type: 'image/webp' }) 9 | } 10 | -------------------------------------------------------------------------------- /utils/formatNumber.ts: -------------------------------------------------------------------------------- 1 | export const formatNumber = (num: number) => { 2 | if (num >= 1_000_000) { 3 | return (num / 1_000_000).toFixed(1) + 'M' 4 | } else if (num >= 10_000) { 5 | return (num / 10_000).toFixed(1) + 'w' 6 | } else if (num >= 1_000) { 7 | return (num / 1_000).toFixed(1) + 'k' 8 | } else { 9 | return num.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /utils/kunCopy.ts: -------------------------------------------------------------------------------- 1 | import toast from 'react-hot-toast' 2 | 3 | const decodeIfEncoded = (text: string) => { 4 | try { 5 | const decoded = decodeURIComponent(text) 6 | return decoded !== text ? decoded : text 7 | } catch (e) { 8 | return text 9 | } 10 | } 11 | 12 | export const kunCopy = (originText: string) => { 13 | const text = decodeIfEncoded(originText) 14 | 15 | navigator.clipboard 16 | .writeText(text) 17 | .then(() => 18 | toast.success(`${text} 复制成功`, { 19 | style: { 20 | whiteSpace: 'pre-wrap', 21 | wordBreak: 'break-all' 22 | } 23 | }) 24 | ) 25 | .catch(() => toast.error('复制失败! 请更换更现代的浏览器!')) 26 | } 27 | -------------------------------------------------------------------------------- /utils/lz.ts: -------------------------------------------------------------------------------- 1 | declare module 'lz-string' { 2 | export function compressToBase64(input: string): string 3 | export function decompressFromBase64(input: string): string 4 | 5 | export function compressToUTF16(input: string): string 6 | export function decompressFromUTF16(compressed: string): string 7 | 8 | export function compressToUint8Array(uncompressed: string): Uint8Array 9 | export function decompressFromUint8Array(compressed: Uint8Array): string 10 | 11 | export function compressToEncodedURIComponent(input: string): string 12 | export function decompressFromEncodedURIComponent(compressed: string): string 13 | 14 | export function compress(input: string): string 15 | export function decompress(compressed: string): string 16 | } 17 | 18 | import { compressToBase64, decompressFromBase64 } from 'lz-string' 19 | 20 | export const encode = compressToBase64 21 | 22 | export const decode = decompressFromBase64 23 | -------------------------------------------------------------------------------- /utils/markdownToText.tsx: -------------------------------------------------------------------------------- 1 | export const markdownToText = (markdown: string) => { 2 | return markdown 3 | .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') 4 | .replace(/!\[([^\]]*)\]\([^\)]+\)/g, '$1') 5 | .replace(/(\*\*|__)(.*?)\1/g, '$2') 6 | .replace(/(\*|_)(.*?)\1/g, '$2') 7 | .replace(/^\s*(#{1,6})\s+(.*)/gm, '$2') 8 | .replace(/```[\s\S]*?```|`([^`]*)`/g, '$1') 9 | .replace(/^(-{3,}|\*{3,})$/gm, '') 10 | .replace(/^\s*([-*+]|\d+\.)\s+/gm, '') 11 | .replace(/\n{2,}/g, '\n') 12 | .trim() 13 | } 14 | -------------------------------------------------------------------------------- /utils/random.ts: -------------------------------------------------------------------------------- 1 | export const randomNum = (lowerValue: number, upperValue: number) => { 2 | return Math.floor(Math.random() * (upperValue - lowerValue + 1) + lowerValue) 3 | } 4 | -------------------------------------------------------------------------------- /utils/sanitizeFileName.ts: -------------------------------------------------------------------------------- 1 | export const sanitizeFileName = (fileName: string) => { 2 | const match = fileName.match(/^(.*?)(\.[^.]+)?$/) 3 | if (!match) { 4 | return fileName 5 | } 6 | 7 | const baseName = match[1] 8 | const extension = match[2] || '' 9 | 10 | const sanitizedBaseName = baseName.replace(/[^\p{L}\p{N}_-]/gu, '') 11 | 12 | return `${sanitizedBaseName.slice(0, 100)}${extension}` 13 | } 14 | -------------------------------------------------------------------------------- /utils/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export const hourDiff = (upvoteTime: number, hours: number) => { 4 | if (upvoteTime === 0 || upvoteTime === undefined) { 5 | return false 6 | } 7 | 8 | const currentTime = dayjs() 9 | 10 | const time = dayjs(upvoteTime) 11 | 12 | return currentTime.diff(time, 'hour') <= hours 13 | } 14 | 15 | export const formatDate = ( 16 | time: Date | number | string, 17 | config?: { isShowYear?: boolean; isPrecise?: boolean } 18 | ): string => { 19 | let formatString = 'MM-DD' 20 | 21 | if (config?.isShowYear) { 22 | formatString = 'YYYY-MM-DD' 23 | } 24 | 25 | if (config?.isPrecise) { 26 | formatString = `${formatString} - HH:mm` 27 | } 28 | 29 | return dayjs(time).format(formatString) 30 | } 31 | -------------------------------------------------------------------------------- /validations/comment.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const commentSchema = z.object({ 4 | sortField: z.union([z.literal('created'), z.literal('like')]), 5 | sortOrder: z.union([z.literal('asc'), z.literal('desc')]), 6 | page: z.coerce.number().min(1).max(9999999), 7 | limit: z.coerce.number().min(1).max(50) 8 | }) 9 | -------------------------------------------------------------------------------- /validations/galgame.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const galgameSchema = z.object({ 4 | selectedType: z.string().min(1).max(107), 5 | sortField: z.union([ 6 | z.literal('resource_update_time'), 7 | z.literal('created'), 8 | z.literal('view'), 9 | z.literal('download') 10 | ]), 11 | sortOrder: z.union([z.literal('asc'), z.literal('desc')]), 12 | page: z.coerce.number().min(1).max(9999999), 13 | limit: z.coerce.number().min(1).max(24), 14 | yearString: z.string().max(1007), 15 | monthString: z.string().max(1007) 16 | }) 17 | -------------------------------------------------------------------------------- /validations/message.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { MESSAGE_TYPE } from '~/constants/message' 3 | 4 | export const createMessageSchema = z.object({ 5 | type: z.enum(MESSAGE_TYPE), 6 | content: z 7 | .string() 8 | .url('请输入有效的链接格式') 9 | .max(1007, { message: '单个链接的长度最大 1007 个字符' }), 10 | recipientId: z.coerce.number().min(1).max(9999999), 11 | link: z.string().max(1007) 12 | }) 13 | 14 | export const getMessageSchema = z.object({ 15 | type: z.enum(MESSAGE_TYPE).optional(), 16 | page: z.coerce.number().min(1).max(9999999), 17 | limit: z.coerce.number().min(1).max(30) 18 | }) 19 | -------------------------------------------------------------------------------- /validations/release.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const getReleaseSchema = z.object({ 4 | year: z.coerce.number().min(1).max(5000), 5 | month: z.coerce.number().min(1).max(12) 6 | }) 7 | -------------------------------------------------------------------------------- /validations/resource.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const resourceSchema = z.object({ 4 | sortField: z.union([ 5 | z.literal('update_time'), 6 | z.literal('created'), 7 | z.literal('download'), 8 | z.literal('like') 9 | ]), 10 | sortOrder: z.union([z.literal('asc'), z.literal('desc')]), 11 | page: z.coerce.number().min(1).max(9999999), 12 | limit: z.coerce.number().min(1).max(50) 13 | }) 14 | -------------------------------------------------------------------------------- /validations/search.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const searchSchema = z.object({ 4 | query: z 5 | .array( 6 | z 7 | .string() 8 | .trim() 9 | .min(1) 10 | .max(107, { message: '单个搜索关键词最大长度为 107' }) 11 | ) 12 | .min(1) 13 | .max(10, { message: '您最多使用 10 组关键词' }), 14 | page: z.coerce.number().min(1).max(9999999), 15 | limit: z.coerce.number().min(1).max(24), 16 | searchOption: z.object({ 17 | searchInIntroduction: z.boolean().default(false), 18 | searchInAlias: z.boolean().default(false), 19 | searchInTag: z.boolean().default(false) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /validations/walkthrough.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const createWalkthroughSchema = z.object({ 4 | patchId: z.coerce.number().min(1).max(9999999), 5 | name: z 6 | .string() 7 | .min(3, { message: '攻略标题最少三个字' }) 8 | .max(233, { message: '攻略标题最多 233 字' }), 9 | content: z 10 | .string() 11 | .min(3, { message: '攻略内容最少三个字' }) 12 | .max(100007, { message: '攻略内容最多 100007 字' }) 13 | }) 14 | 15 | export const updateWalkthroughSchema = z.object({ 16 | walkthroughId: z.coerce.number().min(1).max(9999999), 17 | name: z 18 | .string() 19 | .min(3, { message: '攻略标题最少三个字' }) 20 | .max(233, { message: '攻略标题最多 233 字' }), 21 | content: z 22 | .string() 23 | .min(3, { message: '攻略内容最少三个字' }) 24 | .max(100007, { message: '攻略内容最多 100007 字' }) 25 | }) 26 | --------------------------------------------------------------------------------