├── .dockerignore ├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── discord.xml ├── inspectionProfiles │ └── Project_Default.xml ├── mccade-frontend.iml ├── modules.xml ├── runConfigurations │ ├── Debug_Node.xml │ └── Debug_Server.xml └── vcs.xml ├── .vscode ├── launch.json ├── settings.json └── tailwind.json ├── Dockerfile ├── LICENSE ├── README.md ├── imgs ├── image-1.png ├── image-3.png ├── image-4.png ├── image-5.png ├── image-6.png ├── image-7.png └── image.png ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── img │ ├── banner.png │ ├── discord-logo.png │ ├── fem-alex-head.png │ ├── fem-alex.png │ ├── header-bg.png │ ├── logo-simple.png │ ├── logo.png │ ├── mccade-banner.png │ ├── mccade-stamp.png │ ├── placeholder.png │ ├── skywars-bg.webp │ └── thread │ │ └── 1e6e6e62-e23c-4d25-8833-77b3046864bb.jpeg └── js │ └── markdown.js ├── src ├── app │ ├── (admin) │ │ ├── layout.tsx │ │ ├── op │ │ │ └── page.tsx │ │ └── styles.css │ ├── (main) │ │ ├── (home) │ │ │ ├── Bullet.tsx │ │ │ ├── HomeHeader.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── styles.css │ │ ├── (layout-components) │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── MobileNavbarContent.tsx │ │ │ ├── NavLinks.tsx │ │ │ ├── UserDropdown.tsx │ │ │ └── styles.css │ │ ├── (withHeaderContent) │ │ │ ├── (info) │ │ │ │ ├── Category.tsx │ │ │ │ ├── faq │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── privacy │ │ │ │ │ └── page.tsx │ │ │ │ ├── rules │ │ │ │ │ ├── forum │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── global │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── network │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── styles.css │ │ │ │ └── terms │ │ │ │ │ └── page.tsx │ │ │ ├── HeaderContent.tsx │ │ │ ├── account │ │ │ │ ├── notifications │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── settings │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ ├── component.tsx │ │ │ │ │ ├── delete │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ ├── layout.tsx │ │ │ │ └── panel │ │ │ │ │ ├── general │ │ │ │ │ ├── DataForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.css │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── tickets │ │ │ │ │ ├── TicketSearch.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.css │ │ │ ├── auth │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ │ └── page.tsx │ │ │ │ ├── register │ │ │ │ │ └── page.tsx │ │ │ │ ├── reset │ │ │ │ │ └── page.tsx │ │ │ │ ├── styles.css │ │ │ │ └── template.tsx │ │ │ ├── forums │ │ │ │ ├── (components) │ │ │ │ │ ├── AsideInfo.tsx │ │ │ │ │ ├── Category.tsx │ │ │ │ │ ├── CreateThread │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ ├── component.tsx │ │ │ │ │ │ └── styles.css │ │ │ │ │ ├── Forum.tsx │ │ │ │ │ ├── Navigation.tsx │ │ │ │ │ ├── RouteSegmentNav.tsx │ │ │ │ │ ├── SideOptions.tsx │ │ │ │ │ └── ThreadInfo.tsx │ │ │ │ ├── Utils.ts │ │ │ │ ├── [forumId] │ │ │ │ │ ├── Thread.tsx │ │ │ │ │ ├── [threadId] │ │ │ │ │ │ ├── Replies.tsx │ │ │ │ │ │ ├── ReplyForm.tsx │ │ │ │ │ │ ├── deleteHard │ │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── deleteSoft │ │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── lock │ │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── styles.css │ │ │ │ │ │ └── unlock │ │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── createCategory │ │ │ │ │ ├── CreateCategoryForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── createForum │ │ │ │ │ ├── CreateForumForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── deleteCategory │ │ │ │ │ ├── DeleteCategoryForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── deleteForum │ │ │ │ │ ├── DeleteForumForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── lockForum │ │ │ │ │ ├── LockForumForm.tsx │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── styles.css │ │ │ │ └── unlockForum │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ ├── UnlockForumForm.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── staff │ │ │ │ └── page.tsx │ │ │ ├── styles.css │ │ │ ├── support │ │ │ │ ├── (components) │ │ │ │ │ ├── CreateTicket │ │ │ │ │ │ ├── component.tsx │ │ │ │ │ │ ├── createTicket.ts │ │ │ │ │ │ └── styles.css │ │ │ │ │ ├── Navigation.tsx │ │ │ │ │ ├── Questions.ts │ │ │ │ │ ├── RouteSegmentNav.tsx │ │ │ │ │ ├── Table │ │ │ │ │ │ ├── Table.tsx │ │ │ │ │ │ ├── TableEntry.tsx │ │ │ │ │ │ ├── TableHeader.tsx │ │ │ │ │ │ └── styles.css │ │ │ │ │ └── TicketComponent.tsx │ │ │ │ ├── Utils.ts │ │ │ │ ├── [page] │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── styles.css │ │ │ │ └── tickets │ │ │ │ │ └── [ticketId] │ │ │ │ │ ├── Replies.tsx │ │ │ │ │ ├── ReplyForm.tsx │ │ │ │ │ ├── TicketActions.tsx │ │ │ │ │ ├── TicketServerActions.ts │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.css │ │ │ └── u │ │ │ │ ├── [username] │ │ │ │ ├── SocialConnections.tsx │ │ │ │ ├── forums │ │ │ │ │ └── page.tsx │ │ │ │ ├── general │ │ │ │ │ ├── StatsWidget.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── staff │ │ │ │ │ ├── grants │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── identity │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── punishments │ │ │ │ │ │ ├── ServerActions.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── statistics │ │ │ │ │ └── page.tsx │ │ │ │ └── styles.css │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── styles.css │ ├── api │ │ └── auth │ │ │ ├── mcNameToUuid │ │ │ └── route.ts │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── robots.ts │ └── sitemap.tsx ├── components │ ├── ClipboardTooltip │ │ ├── ClipboardTooltip.tsx │ │ └── styles.css │ ├── DiscordWidget │ │ ├── component.tsx │ │ └── styles.css │ ├── Dropdown.tsx │ ├── HashLink.tsx │ ├── HeaderContext.tsx │ ├── Logo.tsx │ ├── Minecraft │ │ ├── Client.tsx │ │ ├── MCServerWidget │ │ │ ├── component.tsx │ │ │ └── styles.css │ │ ├── Server.tsx │ │ ├── base.tsx │ │ └── styles.css │ ├── MobileNavbar │ │ ├── component.tsx │ │ └── styles.css │ ├── NavLink │ │ ├── component.tsx │ │ └── styles.css │ ├── NewsWidget │ │ ├── component.tsx │ │ └── styles.css │ ├── ShrinkableSearch │ │ ├── component.tsx │ │ └── styles.css │ ├── Table │ │ ├── Table.tsx │ │ ├── TableEntry.tsx │ │ ├── TableHeader.tsx │ │ └── styles.css │ └── ThemeToggle.tsx ├── hooks │ ├── useGlobal.ts │ ├── useHash.ts │ ├── useMcUuid.ts │ ├── useSession.ts │ └── useTheme.ts ├── libs │ ├── HTTPClient.ts │ ├── Utils.ts │ ├── session │ │ ├── getSession.ts │ │ └── iron.ts │ └── types │ │ ├── entities │ │ ├── Account.ts │ │ ├── Forum.ts │ │ ├── ForumCategory.ts │ │ ├── Grant.ts │ │ ├── Permission.ts │ │ ├── Profile.ts │ │ ├── ProfileConnections.ts │ │ ├── Punishment.ts │ │ ├── Rank.ts │ │ ├── Scope.ts │ │ ├── SkywarsStats.ts │ │ ├── TextFilter.ts │ │ ├── Thread.ts │ │ ├── Ticket.ts │ │ ├── TicketCategory.ts │ │ ├── TicketReply.ts │ │ ├── User.ts │ │ └── WebEntry.ts │ │ └── extensions │ │ └── lib.dom.iterable.ts ├── middleware.ts └── services │ ├── base │ └── ServerStatsService.ts │ ├── controller │ ├── ChatSnapshotService.ts │ ├── CommandLogService.ts │ ├── GrantService.ts │ ├── LeaderboardService.ts │ ├── NetworkService.ts │ ├── NotificationService.ts │ ├── ProfileService.ts │ ├── PunishmentService.ts │ ├── RankListService.ts │ └── RankService.ts │ ├── forum │ ├── account │ │ └── AccountService.ts │ ├── category │ │ └── CategoryService.ts │ ├── filter │ │ └── TextFilterService.ts │ ├── forum │ │ └── ForumService.ts │ ├── punishment │ │ └── PunishmentService.ts │ ├── search │ │ └── SearchService.ts │ ├── stats │ │ └── GameStatsService.ts │ ├── thread │ │ └── ThreadService.ts │ ├── ticket │ │ ├── TicketCategoryService.ts │ │ └── TicketService.ts │ ├── trophy │ │ └── TrophyService.ts │ └── websiteData │ │ └── WebsiteDataService.ts │ └── totp │ └── TotpService.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | node_modules 3 | npm-debug.log 4 | README.md 5 | .next 6 | .git 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,ts,jsx,tsx,css}] 4 | insert_final_newline = true 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Insert here default values regardless of environment. 2 | ## Obs.: THIS FILE IS COMMITTED TO GITHUB! Secrets should alwas go in .env.local. 3 | ## NEXT_PUBLIC_ prefix is used to expose variables to the browser, use with caution! 4 | 5 | NEXT_PUBLIC_DISCORD_URL=https://discord.gg/playmccade 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # Insert here default values for the production environment. 2 | ## Obs.: THIS FILE IS COMMITTED TO GITHUB! Secrets should alwas go in .env.local. 3 | ## NEXT_PUBLIC_ prefix is used to expose variables to the browser, use with caution! 4 | 5 | NEXT_PUBLIC_CURR_DOMAIN=http://localhost:3000 6 | API_URL=http://100.105.201.65:40002 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # Insert here default values for the production environment. 2 | ## Obs.: THIS FILE IS COMMITTED TO GITHUB! Secrets should alwas go in .env.local. 3 | ## NEXT_PUBLIC_ prefix is used to expose variables to the browser, use with caution! 4 | 5 | NEXT_PUBLIC_CURR_DOMAIN=https://www.mccade.net/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unescaped-entities": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.x 18 | 19 | - name: Install dependencies 20 | run: yarn install 21 | 22 | - name: Lint 23 | run: yarn run lint 24 | -------------------------------------------------------------------------------- /.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 | # Sentry Config File 39 | .sentryclirc 40 | 41 | # Qodana config 42 | qodana.yaml -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/mccade-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Debug_Node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Debug_Server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "yarn dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "webRoot": "${workspaceFolder}/public", 15 | "url": "http://localhost:3000", 16 | "runtimeExecutable": "/usr/lib/chromium/chromium" 17 | }, 18 | { 19 | "name": "Next.js: debug full stack", 20 | "type": "node-terminal", 21 | "request": "launch", 22 | "command": "yarn dev", 23 | "serverReadyAction": { 24 | "pattern": "started server on .+, url: (https?://.+)", 25 | "uriFormat": "%s", 26 | "action": "debugWithChrome" 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": [".vscode/tailwind.json"] 3 | } -------------------------------------------------------------------------------- /.vscode/tailwind.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@apply", 16 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 17 | "references": [ 18 | { 19 | "name": "Tailwind Documentation", 20 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@responsive", 26 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@screen", 36 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "@variants", 46 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 47 | "references": [ 48 | { 49 | "name": "Tailwind Documentation", 50 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 51 | } 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 6 | RUN apk add --no-cache libc6-compat 7 | WORKDIR /app 8 | 9 | # Install dependencies based on the preferred package manager 10 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 11 | RUN \ 12 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 13 | elif [ -f package-lock.json ]; then npm ci; \ 14 | elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ 15 | else echo "Lockfile not found." && exit 1; \ 16 | fi 17 | 18 | # Rebuild the source code only when needed 19 | FROM base AS builder 20 | WORKDIR /app 21 | COPY --from=deps /app/node_modules ./node_modules 22 | COPY . . 23 | 24 | # Next.js collects completely anonymous telemetry data about general usage. 25 | # Learn more here: https://nextjs.org/telemetry 26 | # Uncomment the following line in case you want to disable telemetry during the build. 27 | ENV NEXT_TELEMETRY_DISABLED 1 28 | 29 | RUN yarn build 30 | 31 | # If using npm comment out above and use below instead 32 | # RUN npm run build 33 | 34 | # Production image, copy all the files and run next 35 | FROM base AS runner 36 | WORKDIR /app 37 | 38 | ENV NODE_ENV production 39 | # Uncomment the following line in case you want to disable telemetry during runtime. 40 | ENV NEXT_TELEMETRY_DISABLED 1 41 | 42 | RUN addgroup --system --gid 1001 nodejs 43 | RUN adduser --system --uid 1001 nextjs 44 | 45 | COPY --from=builder /app/public ./public 46 | 47 | # Set the correct permission for prerender cache 48 | RUN mkdir .next 49 | RUN chown nextjs:nodejs .next 50 | 51 | # Automatically leverage output traces to reduce image size 52 | # https://nextjs.org/docs/advanced-features/output-file-tracing 53 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 54 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 55 | 56 | USER nextjs 57 | 58 | EXPOSE 3000 59 | 60 | ENV PORT 3000 61 | # set hostname to localhost 62 | ENV HOSTNAME "0.0.0.0" 63 | 64 | # server.js is created by next build from the standalone output 65 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 66 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 4-Clause License 2 | 3 | Copyright (c) 2024, LunarLabs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. All advertising materials mentioning features or use of this software must 17 | display the following acknowledgement: 18 | This product includes software developed by LunarLabs. 19 | 20 | 4. Neither the name of the copyright holder nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR 25 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 26 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 27 | EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 29 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 30 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 31 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 32 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 33 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /imgs/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-1.png -------------------------------------------------------------------------------- /imgs/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-3.png -------------------------------------------------------------------------------- /imgs/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-4.png -------------------------------------------------------------------------------- /imgs/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-5.png -------------------------------------------------------------------------------- /imgs/image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-6.png -------------------------------------------------------------------------------- /imgs/image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image-7.png -------------------------------------------------------------------------------- /imgs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/imgs/image.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const mcStats = { 4 | protocol: 'https', 5 | hostname: 'skins.mcstats.com', 6 | port: '', 7 | }; 8 | const images = { 9 | minimumCacheTTL: 60, 10 | remotePatterns: [ 11 | {...mcStats, pathname: '/bust/**'}, 12 | {...mcStats, pathname: '/skull/**'}, 13 | ], 14 | }; 15 | const nextConfig = { 16 | output: 'standalone', 17 | // reactStrictMode: false, 18 | 19 | images: images, 20 | } 21 | 22 | module.exports = nextConfig -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voyager-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "daisyui": "^4.4.24", 13 | "dompurify": "^3.0.9", 14 | "iron-session": "^8.0.1", 15 | "isomorphic-dompurify": "^2.4.0", 16 | "marked": "^12.0.1", 17 | "next": "^14.1.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-icons": "^4.12.0", 21 | "react-intersection-observer": "^9.8.1", 22 | "react-spinners": "^0.13.8", 23 | "reactjs-popup": "^2.0.6", 24 | "rxjs": "^7.8.1", 25 | "sharp": "^0.33.2", 26 | "zod": "^3.22.4" 27 | }, 28 | "devDependencies": { 29 | "@types/dompurify": "^3.0.5", 30 | "@types/node": "latest", 31 | "@types/react": "latest", 32 | "@types/react-dom": "latest", 33 | "autoprefixer": "^10.0.1", 34 | "eslint": "^8", 35 | "eslint-config-next": "^14.1.0", 36 | "postcss": "^8", 37 | "tailwindcss": "^3.3.0", 38 | "typescript": "^5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/banner.png -------------------------------------------------------------------------------- /public/img/discord-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/discord-logo.png -------------------------------------------------------------------------------- /public/img/fem-alex-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/fem-alex-head.png -------------------------------------------------------------------------------- /public/img/fem-alex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/fem-alex.png -------------------------------------------------------------------------------- /public/img/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/header-bg.png -------------------------------------------------------------------------------- /public/img/logo-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/logo-simple.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/logo.png -------------------------------------------------------------------------------- /public/img/mccade-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/mccade-banner.png -------------------------------------------------------------------------------- /public/img/mccade-stamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/mccade-stamp.png -------------------------------------------------------------------------------- /public/img/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/placeholder.png -------------------------------------------------------------------------------- /public/img/skywars-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/skywars-bg.webp -------------------------------------------------------------------------------- /public/img/thread/1e6e6e62-e23c-4d25-8833-77b3046864bb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/img/thread/1e6e6e62-e23c-4d25-8833-77b3046864bb.jpeg -------------------------------------------------------------------------------- /public/js/markdown.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/public/js/markdown.js -------------------------------------------------------------------------------- /src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import type { Metadata } from 'next'; 3 | 4 | const title = 'MCCade - M.O.T.H.E.R.'; 5 | const description = 'Metrics Overview Tracking & Health Evaluation Reporting.'; 6 | export const metadata: Metadata = { 7 | title: title, 8 | description: description, 9 | } 10 | export default function Layout({ 11 | children, 12 | }: { 13 | children: React.ReactNode 14 | }) { 15 | return <> 16 | {children} 17 | ; 18 | } 19 | //#region Metadata -------------------------------------------------------------------------------- /src/app/(admin)/op/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return <> 3 | ; 4 | } -------------------------------------------------------------------------------- /src/app/(admin)/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/src/app/(admin)/styles.css -------------------------------------------------------------------------------- /src/app/(main)/(home)/Bullet.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import "./styles.css" 4 | 5 | export interface Props { 6 | color: string 7 | } 8 | 9 | export default function Bullet(props: Props) { 10 | const { color } = props; 11 | 12 | return <> 13 | 20 |
21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/(main)/(home)/HomeHeader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCallback } from 'react'; 4 | import ThemeToggle from "@/components/ThemeToggle"; 5 | import { MobileNavToggle } from "@/components/MobileNavbar/component"; 6 | import ShrinkableSearch from "@/components/ShrinkableSearch/component"; 7 | import NavLinks from '../(layout-components)/NavLinks'; 8 | import UserDropdown from '../(layout-components)/UserDropdown'; 9 | import Logo from '@/components/Logo'; 10 | 11 | export interface Props { 12 | isStaff: boolean 13 | } 14 | 15 | const Header = (props: Props) => { 16 | const { isStaff } = props; 17 | 18 | const defaultVal = useCallback(() => (['', ''] as [string, string]), []); 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | 36 |
37 | {/* */} 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Header; 50 | -------------------------------------------------------------------------------- /src/app/(main)/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | 3 | export default function Layout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return <> 9 | {children} 10 | ; 11 | } -------------------------------------------------------------------------------- /src/app/(main)/(home)/styles.css: -------------------------------------------------------------------------------- 1 | /* #region HomeHeader */ 2 | .bg-banner { 3 | background-image: url('/img/mccade-banner.png'); 4 | } 5 | 6 | .filter-home { 7 | backdrop-filter: brightness(85%) contrast(120%); 8 | } 9 | 10 | .text-stroke { 11 | --thickness: .4px; 12 | --filter: drop-shadow(0px 0px var(--thickness) rgba(0,0,0,1)); 13 | filter: var(--filter) var(--filter) var(--filter) var(--filter); 14 | } 15 | 16 | .header-logo img { 17 | margin-top: -25px; 18 | width: 100%; 19 | max-width: 225px; 20 | height: auto; 21 | transition: margin 0.25s ease, width 0.25s ease, max-width 0.25s ease; 22 | animation: header-logo 2s ease-in-out infinite both alternate; 23 | filter: drop-shadow(0px 4px 0px #00000045) drop-shadow(0px 0px 20px #00000073); 24 | } 25 | 26 | @keyframes header-logo { 27 | 100% { 28 | transform: scale3d(0.94, 0.94, 0.94); 29 | } 30 | } 31 | 32 | .home-inner { 33 | height: 65px; 34 | width: 100%; 35 | transition: top 0.2s ease-in-out; 36 | position: absolute; 37 | z-index: 2; 38 | inset: 0; 39 | } 40 | 41 | .home-inner .content .dropdown-custom { 42 | @apply dropdown-end; 43 | } 44 | .home-inner .content .mobile-nav-toggle { 45 | display: none; 46 | } 47 | 48 | @media (max-width: 865px) { 49 | .home-inner .content .navlink { 50 | display: none; 51 | } 52 | .home-inner .content .mobile-nav-toggle { 53 | display: inline-flex; 54 | } 55 | } 56 | @media (max-width: 410px) { 57 | .home-inner .content .theme-toggle { 58 | display: none; 59 | } 60 | .home-inner .content .dropdown-custom { 61 | display: none; 62 | } 63 | } 64 | /* #endregion */ 65 | 66 | .home-h { 67 | --h-diff: 0px; 68 | min-height: calc(min(1920px, 100vh) - var(--h-diff)); 69 | width: 100%; 70 | } 71 | 72 | .home-border { 73 | --border-color: rgba(50, 50, 50, 1); 74 | border:solid 1.5px var(--border-color) 75 | } 76 | 77 | .news-widget.main > div { 78 | @apply md:flex-nowrap; 79 | 80 | } 81 | 82 | .news-container p { 83 | word-wrap:break-word; 84 | } 85 | 86 | @media (max-width: 1278px) { 87 | .news-container > div{ 88 | flex-wrap: wrap; 89 | } 90 | } 91 | 92 | .bullet { 93 | position: relative; 94 | display: inline-block; 95 | width: 16px; 96 | height: 16px; 97 | border-radius: 50%; 98 | flex: none; 99 | } 100 | .bullet::before, .bullet::after { 101 | content: ''; 102 | position: absolute; 103 | inset: 0; 104 | /* background: oklch(var(--su)); */ 105 | border-radius: 50%; 106 | } 107 | .bullet::before { 108 | opacity: 0.25; 109 | } 110 | .bullet::after{ 111 | transform: scale3d(.59, .59, .59); 112 | } 113 | -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/Footer.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import Image from "next/image"; 3 | import Link from 'next/link'; 4 | import { FaCloud, FaDiscord, FaTelegram, FaTwitter } from "react-icons/fa6"; 5 | 6 | const Footer = () => ( 7 | 42 | ); 43 | 44 | export default Footer; 45 | -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/Header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import './styles.css' 3 | import NavLinks from "./NavLinks"; 4 | import ThemeToggle from "@/components/ThemeToggle"; 5 | import { MobileNavToggle } from "@/components/MobileNavbar/component"; 6 | import ShrinkableSearch from "@/components/ShrinkableSearch/component"; 7 | import UserDropdown from "./UserDropdown" 8 | import { usePathname } from 'next/navigation'; 9 | 10 | export interface Props { 11 | isStaff: boolean 12 | } 13 | 14 | const Header = (props: Props) => { 15 | const { isStaff } = props; 16 | 17 | const path = usePathname(); 18 | return ( 19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 | {/* */} 31 | 32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default Header; 43 | -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/MobileNavbarContent.tsx: -------------------------------------------------------------------------------- 1 | import NavLinks from './NavLinks'; 2 | import Logo from '@/components/Logo'; 3 | import ThemeToggle from '@/components/ThemeToggle'; 4 | import UserDropdown from './UserDropdown'; 5 | import getSession from '@/libs/session/getSession'; 6 | import { getHighestRank } from '@/services/controller/GrantService'; 7 | 8 | const MobileNavbarContent = async () => { 9 | const session = await getSession(); 10 | const isStaff = (await getHighestRank(session.uuid))?.staff || false; 11 | 12 | return <> 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 | 24 | } 25 | 26 | export default MobileNavbarContent; 27 | -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/NavLinks.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import NavLink from "@/components/NavLink/component"; 3 | import { useCallback, useState } from "react"; 4 | import { ClipLoader } from "react-spinners"; 5 | import Link from "next/link"; 6 | import useSession from "@/hooks/useSession"; 7 | import { usePathname } from "next/navigation"; 8 | 9 | export interface Props { 10 | isStaff: boolean 11 | } 12 | const NavLinks = (props: Props) => { 13 | const { isStaff } = props; 14 | 15 | const navLinks = [ 16 | { href: "/", text: "Home", icon: <> }, 17 | { href: "/forums", text: "Forums", icon: <> }, 18 | { href: "/staff", text: "Staff", icon: <> }, 19 | { href: "/support", text: "Support", icon: <> }, 20 | ]; 21 | 22 | if (isStaff) 23 | navLinks.push({ href: "/admin/panel", text: "Staff Panel", icon: <> }); 24 | 25 | 26 | return <> 27 | {navLinks.map((navLink, i) => 28 | 29 | {navLink.icon}
{navLink.text}
30 |
31 | )} 32 | ; 33 | }; 34 | 35 | export default NavLinks; 36 | 37 | export const UserNav = () => { 38 | const [isLoading, setIsLoading] = useState(false); 39 | const { session, logout } = useSession(); 40 | const path = usePathname(); 41 | 42 | const logoutCall = useCallback(() => { 43 | setIsLoading(true); 44 | logout().then(() => { 45 | setIsLoading(false); 46 | }); 47 | }, [logout]); 48 | 49 | const params = new URLSearchParams({ redirect: path }) 50 | return isLoading ? ( 51 | 52 | ) : <> 53 | { 54 | !session?.isLoggedIn ? 55 | <> 56 | Login 57 | Register 58 | : 59 | <> 60 | Profile 61 | Settings 62 | Logout 63 | 64 | } 65 | ; 66 | } 67 | -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/UserDropdown.tsx: -------------------------------------------------------------------------------- 1 | import Dropdown from "@/components/Dropdown"; 2 | import { IoPerson } from "react-icons/io5"; 3 | import { UserNav } from "./NavLinks"; 4 | 5 | const UserDropdown = (props: { className?: string }) => 6 | } 9 | dropdownClassName={`shadow rounded-box px-4 py-3 bg-base-300 w-fit h-fit whitespace-nowrap ${props.className || ''}`} 10 | > 11 | 12 | 13 | 14 | export default UserDropdown; -------------------------------------------------------------------------------- /src/app/(main)/(layout-components)/styles.css: -------------------------------------------------------------------------------- 1 | /*#region Header*/ 2 | header .inner::before { 3 | content: ''; 4 | position: absolute; 5 | inset: 0 0 auto 0; 6 | --opacity: .4; 7 | background: linear-gradient(to bottom, oklch(var(--b1)/var(--opacity)) 0%, transparent 100%); 8 | height: 112%; 9 | z-index: -1; 10 | backdrop-filter: blur(2px); 11 | } 12 | [data-theme="dark"] header .inner::before { 13 | --opacity: .7; 14 | } 15 | header .inner { 16 | height: 65px; 17 | width: 100%; 18 | transition: top 0.2s ease-in-out; 19 | position: fixed; 20 | z-index: 2; 21 | inset: 0; 22 | } 23 | 24 | header .inner .content .dropdown-custom { 25 | @apply dropdown-end; 26 | } 27 | header .inner .content .mobile-nav-toggle { 28 | display: none; 29 | } 30 | 31 | @media (max-width: 865px) { 32 | header .inner .content .navlink { 33 | display: none; 34 | } 35 | header .inner .content .mobile-nav-toggle { 36 | display: inline-flex; 37 | } 38 | } 39 | @media (max-width: 410px) { 40 | header .inner .content .theme-toggle { 41 | display: none; 42 | } 43 | header .inner .content .dropdown-custom { 44 | display: none; 45 | } 46 | } 47 | /*#endregion*/ 48 | 49 | /*#region Footer*/ 50 | @media (max-width: 1179px) { 51 | footer { 52 | flex-direction: column; 53 | align-items: start !important; 54 | padding: 24px !important; 55 | } 56 | 57 | footer .links { 58 | margin: 0; 59 | padding: 0; 60 | } 61 | 62 | .divisor { 63 | background-color: var(--fallback-nc,oklch(var(--b1)/.75)); 64 | display: inline-block; 65 | width: 100%; 66 | height: 1px; 67 | } 68 | } 69 | /*#endregion*/ -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/(info)/Category.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import NavLink from "@/components/NavLink/component"; 3 | 4 | interface CategoryProps { 5 | className?: string; 6 | title: string; 7 | buttons: { 8 | text: string; 9 | route: string; 10 | }[]; 11 | } 12 | const Category = ({ className: className, title: title, buttons: buttons }: CategoryProps) => { 13 | return ( 14 |
15 |
16 | {title} 17 |
18 |
19 | { 20 | buttons.map((b, i) => 21 | 22 |

{b.text}

23 |
24 | ) 25 | } 26 |
27 |
28 | ); 29 | }; 30 | export default Category; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/(info)/faq/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 |
4 |

Frequently Asked Questions

5 | 6 |
7 | 10 | 15 | 19 | 23 | 26 |
27 |
28 | ); 29 | } 30 | 31 | const Question = (props: { question: string; answer: string; }) => ( 32 |
33 | 34 |
35 | {props.question} 36 |
37 |
38 | {props.answer} 39 |
40 |
41 | ); 42 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/(info)/layout.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from '@/components/HeaderContext'; 2 | import Category from './Category'; 3 | 4 | export default function Layout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | const categories = [ 10 | { 11 | title: "Rules", 12 | buttons: [ 13 | { text: "Global Guidelines", route: "/rules/global"}, 14 | { text: "Forum Guidelines", route: "/rules/forum"}, 15 | { text: "Network Guidelines", route: "/rules/network"}, 16 | 17 | ] 18 | }, 19 | { 20 | title: "Legal", 21 | buttons: [ 22 | { text: "Terms of Service", route: "/terms"}, 23 | { text: "Privacy Policy", route: "/privacy"}, 24 | ] 25 | }, 26 | { 27 | title: "Other", 28 | buttons: [ 29 | { text: "FAQ", route: "/faq"}, 30 | ], 31 | className: "col-span-full", 32 | } 33 | ]; 34 | 35 | const headerContent: [string, string] = ["Information", `Here you can gather some info on us and our policies.`]; 36 | return <> 37 | 38 | 39 |
40 |
41 | {categories.map((c, i) => )} 43 |
44 |
46 | {children} 47 |
48 |
49 | ; 50 | } 51 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/(info)/styles.css: -------------------------------------------------------------------------------- 1 | .grid-categories { 2 | display: grid; 3 | flex: 0 0 min-content; 4 | justify-content: center; 5 | --col-sizes: 193px; 6 | grid-template-columns: repeat(auto-fit, minmax(var(--col-sizes), 1fr)); 7 | grid-auto-flow: row dense; 8 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/HeaderContent.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import './styles.css' 4 | import useGlobal from '@/hooks/useGlobal'; 5 | import { useCallback } from 'react'; 6 | 7 | const Header = () => { 8 | const defaultVal = useCallback(() => (['', ''] as [string, string]), []); 9 | const [headerContent] = useGlobal<[string, string]>('headerContent', defaultVal); 10 | 11 | return ( 12 |
13 | 14 |

{headerContent?.[0]}

15 | {headerContent?.[1]} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Header; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/account/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from "@/components/HeaderContext"; 2 | 3 | export default function Page() { 4 | const headerContent: [string, string] = ["Notifications", `Here you can check what's important.`]; 5 | return <> 6 | 7 | ; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import getSession from "@/libs/session/getSession"; 3 | 4 | export default async function Redirect() { 5 | const session = await getSession(); 6 | redirect(`/u/${session.username}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/account/settings/delete/page.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from "@/components/HeaderContext"; 2 | import getSession from "@/libs/session/getSession"; 3 | 4 | 5 | export default async function Page() { 6 | const headerContent: [string, string] = ["Settings", `Configure your account.`]; 7 | 8 | const session = getSession(); 9 | 10 | return <> 11 | 12 |
13 |

Are you sure you want to delete your MCCade Account?

14 |
Please enter your password to confirm.
15 |
16 | 17 | 20 |
21 |
22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/account/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from "@/components/HeaderContext"; 2 | import getSession from "@/libs/session/getSession"; 3 | import { GetProfileFromUuid, GetPublicConnections, updateConnections } from "@/services/controller/ProfileService"; 4 | import SettingsPage from "./component"; 5 | import { getAccountFromUuid } from "@/services/forum/account/AccountService"; 6 | import { redirect } from "next/navigation"; 7 | 8 | export default async function Page() { 9 | const headerContent: [string, string] = ["Settings", `Configure your account.`]; 10 | 11 | const session = await getSession(); 12 | if (!session.uuid) redirect("/"); 13 | 14 | const profile = (await GetProfileFromUuid(session.uuid))[0]!; 15 | const account = (await getAccountFromUuid(session.uuid))[0]!; 16 | 17 | return <> 18 | 19 | 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { GetActiveRanks } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | 5 | 6 | export default async function Layout({ 7 | children, 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | const session = await getSession(); 12 | if (!((await GetActiveRanks(session.uuid))[0] || []).find(r => r.staff)) { 13 | redirect("/"); 14 | } 15 | 16 | return <>{children} 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/general/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError } from "@/libs/Utils"; 4 | import WebEntry from "@/libs/types/entities/WebEntry" 5 | import { updateEntry } from "@/services/forum/websiteData/WebsiteDataService" 6 | 7 | export async function updateDataAction(entries: WebEntry[]): Promise { 8 | 'use server' 9 | 10 | for (let entry of entries) { 11 | const res = await updateEntry(entry); 12 | if (isResultError(res)) 13 | return "Error: " + (res[2] || res[1]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/general/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAllEntries } from "@/services/forum/websiteData/WebsiteDataService" 2 | import DataForm from "./DataForm"; 3 | import { GetForum } from "@/services/forum/forum/ForumService"; 4 | import { GetForumThreads } from "@/services/forum/thread/ThreadService"; 5 | import Thread from "@/libs/types/entities/Thread"; 6 | 7 | export default async function Page() { 8 | 9 | const websiteData = await getAllEntries(); 10 | const dataMap = websiteData.reduce<{[key:string]: string}>((acc,crr) => { 11 | acc[crr._id] = crr.value; return acc;}, {}); 12 | 13 | const announcementsForum = (await GetForum("Announcements"))[0] 14 | 15 | let threads: Thread[]; 16 | 17 | if (!announcementsForum) { 18 | threads = []; 19 | } else { 20 | threads = (await GetForumThreads(announcementsForum._id))[0] || []; 21 | } 22 | 23 | return
24 |
25 |

Website Data

26 | 27 |
28 |
29 | } 30 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/general/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/src/app/(main)/(withHeaderContent)/admin/panel/general/styles.css -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/layout.tsx: -------------------------------------------------------------------------------- 1 | import NavLink from "@/components/NavLink/component" 2 | 3 | 4 | export default async function Layout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | 10 | return
11 |
12 | 13 | General 14 | 15 | 16 | Tickets 17 | 18 |
19 |
20 | {children} 21 |
22 |
23 | } 24 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | 4 | export default async function Page() { 5 | redirect("/admin/panel/general"); 6 | } 7 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/tickets/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetAllTickets } from "@/services/forum/ticket/TicketService"; 2 | import TicketSearch from "./TicketSearch"; 3 | import { GetAllTicketCategories } from "@/services/forum/ticket/TicketCategoryService"; 4 | import { getUsernameFromUuid } from "@/services/forum/account/AccountService"; 5 | import { getHighestRank, getRankColor } from "@/services/controller/GrantService"; 6 | 7 | 8 | export default async function Page() { 9 | const tickets = (await GetAllTickets())[0] || []; 10 | const categories = (await GetAllTicketCategories())[0] || []; 11 | const userCache: {[key:string]: {uuid: string, name: string, color: string}} = {}; 12 | 13 | for (let ticket of tickets) { 14 | if (userCache[ticket.author]) continue; 15 | 16 | userCache[ticket.author] = { 17 | uuid: ticket.author, 18 | name: (await getUsernameFromUuid(ticket.author)) || "Unknown", 19 | color: (await getRankColor((await getHighestRank(ticket.author))?._id || "")), 20 | } 21 | } 22 | 23 | return <> 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/admin/panel/tickets/styles.css: -------------------------------------------------------------------------------- 1 | 2 | .ticket>div { 3 | word-wrap: break-word; 4 | } 5 | 6 | .ticket { 7 | transition: transform 0.5s; 8 | } 9 | 10 | .ticket:hover { 11 | transform: scale(1.01, 1.01); 12 | } 13 | 14 | .ticket-header>div { 15 | font-weight: bold; 16 | } 17 | 18 | div.page-anchor { 19 | text-align: center; 20 | width: 15px; 21 | } 22 | 23 | svg.page-anchor { 24 | width: 28px; 25 | height: 28px; 26 | } 27 | 28 | svg.page-anchor:hover:not([disabled]) { 29 | cursor: pointer; 30 | } 31 | 32 | div.page-anchor.visible:hover { 33 | cursor: pointer; 34 | } 35 | 36 | svg.page-anchor[disabled] { 37 | opacity: 0.4; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import HeaderContext from '@/components/HeaderContext'; 3 | 4 | export default function Layout({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | const headerContent: [string, string] = ["", ``]; 10 | return <> 11 | 12 | 13 |
14 | {children} 15 |
16 | ; 17 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import useSession from "@/hooks/useSession"; 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | import { FormEvent, useState } from "react"; 5 | import HashLink from "@/components/HashLink"; 6 | import { isResultError } from "@/libs/Utils"; 7 | import Link from "next/link"; 8 | 9 | export default function Page() { 10 | const searchParams = useSearchParams(); 11 | const redirectUrl = searchParams.get('redirect') ?? '/'; 12 | 13 | // const setUsername = useContext(AuthContext)?.setUsername; 14 | 15 | const [isLoading, setIsLoading] = useState(false); 16 | const [error, setError] = useState(null); 17 | const { login } = useSession(); 18 | const { push } = useRouter(); 19 | 20 | //setUsername?.(username); 21 | async function onSubmit(event: FormEvent) { 22 | event.preventDefault(); 23 | setIsLoading(true); 24 | setError(null); 25 | 26 | try { 27 | const formData = new FormData(event.currentTarget); 28 | 29 | const res = await login({ username: formData.get("username"), password: formData.get("password") }); 30 | if (isResultError(res, true)) { 31 | throw new Error(res[2] ?? "Unknown error"); 32 | } 33 | 34 | push(redirectUrl); 35 | } catch (error: any) { 36 | setError(error.message); 37 | } finally { 38 | setIsLoading(false); 39 | } 40 | } 41 | 42 | return <> 43 |

Login to MCCade

44 | {error &&

{error}

} 45 |
46 | 48 | 50 | 51 |
52 |
53 | Don't have an account? Register here! 54 | Forgot your password? Click here to reset. 55 | ; 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/auth/styles.css: -------------------------------------------------------------------------------- 1 | .template .bg-banner { 2 | background-image: url('/img/mccade-banner.png'); 3 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/auth/template.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import './styles.css' 3 | import { Suspense, createContext, useState } from "react"; 4 | import { ClientMCBust } from '@/components/Minecraft/Client'; 5 | 6 | type ContextT = { setUsername: React.Dispatch>; } | undefined; 7 | export const AuthContext = createContext(undefined); 8 | export default function Template({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | const [username, setUsername] = useState(); 14 | return <> 15 |
16 | 17 |
18 |
19 |
20 | 21 | {children} 22 | 23 |
24 |
25 | ; 26 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/AsideInfo.tsx: -------------------------------------------------------------------------------- 1 | import Thread from '@/libs/types/entities/Thread'; 2 | import Table from '@/components/Table/Table'; 3 | import TableEntry from '@/components/Table/TableEntry'; 4 | 5 | export interface AsideInfoProps { 6 | title: string; 7 | content: { 8 | thread: Thread; 9 | item: JSX.Element; 10 | }[]; 11 | } 12 | const AsideInfo = (props: AsideInfoProps) => { 13 | const {title, content} = props; 14 | 15 | const headerContent = [ 16 | 17 | {title} 18 | , 19 | ] 20 | return ( 21 | 22 | {content.map(c => 23 | 24 | {c.item} 25 | 26 | )} 27 |
28 | ); 29 | } 30 | export default AsideInfo; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/Category.tsx: -------------------------------------------------------------------------------- 1 | import Forum from '@/libs/types/entities/Forum'; 2 | import '../styles.css' 3 | import Table from '@/components/Table/Table'; 4 | import ForumComponent from './Forum'; 5 | import { getAuthorInfo } from '../Utils'; 6 | 7 | export interface CategoryData { 8 | name: string; 9 | forums: Forum[]; 10 | } 11 | const Category = async (props: CategoryData) => { 12 | const { 13 | name, 14 | forums 15 | } = props; 16 | 17 | const forumComponents: React.ReactNode[] = []; 18 | for (const sf of forums) { 19 | forumComponents.push( 20 | 30 | ); 31 | } 32 | 33 | const header = [ 34 | {name}, 35 | Threads, 36 | Latest thread 37 | ]; 38 | return ( 39 | 40 | {!forums || !forums.length ? 0 forums in this category. : forumComponents} 41 |
42 | ); 43 | } 44 | export default Category; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/CreateThread/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getAllFilters } from "@/services/forum/filter/TextFilterService"; 6 | import { GetForum } from "@/services/forum/forum/ForumService"; 7 | import { CreateThread } from "@/services/forum/thread/ThreadService"; 8 | import { writeFile } from "fs/promises"; 9 | import { join } from "path"; 10 | import { getAuthorInfo } from "../../Utils"; 11 | import { canUseForum } from "@/services/forum/account/AccountService"; 12 | 13 | export async function createThread(formData: FormData, forumId: string) { 14 | 'use server' 15 | 16 | const session = await getSession(); 17 | if (!session) 18 | return "Not logged in" 19 | const forum = (await GetForum(forumId))[0] 20 | if (!forum) 21 | return "Forum not found" 22 | const user = await getAuthorInfo(session.uuid); 23 | if (forum.locked && !user?.rank?.staff) 24 | return "Pemission denied" 25 | if (!(await canUseForum(session.uuid)) && !user?.rank?.staff) 26 | return "Permission denied" 27 | 28 | const body = formData.get("body")?.toString() || ""; 29 | if (body == "") 30 | return "Thread body cannot be empty" 31 | 32 | const image: File | null = formData.get("thumbnail") as unknown as File; 33 | const title = formData.get("title")?.toString() || ""; 34 | if (title == "") 35 | return "Thread title cannot be empty" 36 | 37 | const filters = (await getAllFilters())[0] || []; 38 | 39 | for (let filter of filters) { 40 | if (body.includes(filter.filter)) 41 | return "Body did not pass filter test"; 42 | 43 | if (title.includes(filter.filter)) 44 | return "Title did not pass filter test"; 45 | } 46 | 47 | const id = newUuid(); 48 | 49 | if (image && forum.name == "Announcements") { 50 | const bytes = await image.arrayBuffer(); 51 | 52 | if (bytes.byteLength > 3 * 1024 * 1024) 53 | return "Image is larger than 3MB" 54 | 55 | if (bytes.byteLength < 2 * 1024) 56 | return "Image is too small" 57 | 58 | const buffer = Buffer.from(bytes); 59 | const path = join(process.cwd(), "/public/img/thread/" + id + "." + image.type.split("/")[1]); 60 | await writeFile(path, buffer); 61 | 62 | } 63 | 64 | const author = session.uuid; 65 | await CreateThread(id, title, body, forumId, author) 66 | } 67 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/CreateThread/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/src/app/(main)/(withHeaderContent)/forums/(components)/CreateThread/styles.css -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/Forum.tsx: -------------------------------------------------------------------------------- 1 | import '../styles.css' 2 | import TableEntry from '@/components/Table/TableEntry'; 3 | import HashLink from '@/components/HashLink'; 4 | import Thread from '@/libs/types/entities/Thread'; 5 | import Rank from '@/libs/types/entities/Rank'; 6 | import ThreadInfo from './ThreadInfo'; 7 | import Link from 'next/link'; 8 | 9 | export interface ActivityData { 10 | thread: { 11 | id: string; 12 | title: string; 13 | }; 14 | author: { 15 | username: string; 16 | rank: string; 17 | }; 18 | time: string; 19 | } 20 | 21 | export interface ForumData { 22 | id: string; 23 | name: string; 24 | description: string; 25 | category: string; 26 | categoryName: string; 27 | threadAmount: number; 28 | lastThread?: Thread; 29 | lastThreadAuthor?: {username: string, rank?: Rank}; 30 | } 31 | const Forum = async (props: ForumData) => { 32 | const {id, name, description, threadAmount, lastThread, lastThreadAuthor} = props; 33 | const lastThreadId = lastThread?._id; 34 | 35 | return ( 36 | 37 | 38 | 39 | {name} 40 | 41 | {description} 42 | 43 | 44 | 45 | 46 | {threadAmount} 47 | 48 | 49 | {lastThread ? 50 | : ( 51 | 52 | None. 53 | 54 | )} 55 | 56 | 57 | ); 58 | } 59 | export default Forum; 60 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import RouteSegmentNav from './RouteSegmentNav'; 2 | import { headers } from "next/headers"; 3 | 4 | export default function Navigation({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return <> 10 |
11 | 12 |
13 | {children} 14 |
15 |
16 | ; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/SideOptions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Link from "next/link"; 4 | 5 | export interface SideOption { 6 | name: string, 7 | color: string, 8 | disabled: boolean, 9 | href: string, 10 | } 11 | 12 | export interface Props { 13 | options: SideOption[] 14 | } 15 | 16 | export default function SideOptions(props: Props) { 17 | const { options } = props; 18 | 19 | if (options.length == 0) 20 | return <> 21 | 22 | return
23 | {options.map((opt, i) => 24 | 25 | 31 | 32 | )} 33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/(components)/ThreadInfo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ServerMCHead } from '@/components/Minecraft/Server'; 3 | import HashLink from '@/components/HashLink'; 4 | import Rank from '@/libs/types/entities/Rank'; 5 | import { stringToDate, toLocaleString } from '@/libs/Utils'; 6 | import Link from 'next/link'; 7 | 8 | interface ThreadInfoProps { 9 | id?: string 10 | forumId?: string; 11 | title?: string; 12 | createdAt?: string; 13 | threadAuthor?: { 14 | username?: string; 15 | rank?: Rank; 16 | }; 17 | } 18 | const ThreadInfo = (props: ThreadInfoProps) => { 19 | const {id, forumId, title, threadAuthor, createdAt} = props; 20 | 21 | const getRankColor = (r?: string) => ({ // TODO: Properly get rank color 22 | Owner: "#9F000C", 23 | Developer: "#ff4141" 24 | }[r ?? '']) ?? "#ffffff" 25 | 26 | return ( 27 | 28 | 29 | 30 | "{title}" 31 | 32 | {threadAuthor?.username} 33 | 34 | 35 | 36 | 37 | 38 | 39 | {toLocaleString(stringToDate(createdAt))} 40 | 41 | ); 42 | } 43 | export default ThreadInfo; 44 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/Utils.ts: -------------------------------------------------------------------------------- 1 | import { GetProfileFromUuid } from '@/services/controller/ProfileService'; 2 | import { GetActiveRanks } from '@/services/controller/GrantService'; 3 | import Rank from '@/libs/types/entities/Rank'; 4 | import { isResultError, tryParseInt } from '@/libs/Utils'; 5 | import Thread from '@/libs/types/entities/Thread'; 6 | 7 | export const getAuthorInfo = async (authorId?: string) => { 8 | if (!authorId) return; 9 | 10 | const author: {username: string, rank?: Rank} = {username: '', rank: undefined}; 11 | const profilePromise = GetProfileFromUuid(authorId) 12 | .then(res => { 13 | if (!isResultError(res)) return res; 14 | console.error("Error fetching author: HTTP " + res[1]); 15 | }); 16 | const ranksPromise = GetActiveRanks(authorId) 17 | .then(res => { 18 | if (!isResultError(res)) return res; 19 | console.error("Error fetching author rank: HTTP " + res[1]); 20 | }); 21 | 22 | const profile = await profilePromise; 23 | if (!profile) return; 24 | author.username = profile[0]!.name; 25 | 26 | const ranks = await ranksPromise; 27 | if (ranks) 28 | author.rank = ranks[0]!.sort((a, b) => a.priority - b.priority)[ranks[0]!.length - 1]; 29 | 30 | return author; 31 | }; 32 | 33 | export const threadSorter = (a?: Thread, b?: Thread) => { 34 | const intA = tryParseInt(a?.createdAt); 35 | const intB = tryParseInt(b?.createdAt); 36 | return intA && intB ? intB - intA : 0; 37 | }; 38 | 39 | export const getThreadShortId = (id?: string) => { 40 | const temp = id?.split('.'); 41 | return temp?.[temp.length - 1]; 42 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/Thread.tsx: -------------------------------------------------------------------------------- 1 | import { ServerMCHead } from '@/components/Minecraft/Server'; 2 | import TableEntry from '@/components/Table/TableEntry'; 3 | import NavLink from '@/components/NavLink/component'; 4 | import Thread from '@/libs/types/entities/Thread'; 5 | import { stringToDate, toLocaleString } from '@/libs/Utils'; 6 | import { getAuthorInfo, threadSorter } from '../Utils'; 7 | 8 | export interface ThreadData { 9 | id: string; 10 | authorId: string; // UUID 11 | title: string; 12 | createdAt: string; 13 | replies: Thread[]; 14 | forumId: string; 15 | } 16 | const ThreadComponent = async (props: ThreadData) => { 17 | const { 18 | id, 19 | authorId, 20 | title, 21 | createdAt, 22 | replies, 23 | forumId, 24 | } = props; 25 | const thisThreadId = id + '.' + forumId; 26 | const authorPromise = getAuthorInfo(authorId); 27 | 28 | const lastReply = replies.sort(threadSorter)[0]; 29 | const lastReplyAuthor = await getAuthorInfo(lastReply?.author); 30 | 31 | const author = await authorPromise 32 | 33 | const getRankColor = (r?: string) => ({ // TODO: Properly get rank color 34 | Owner: "#9F000C", 35 | Developer: "#ff4141" 36 | }[r ?? '']) ?? "#ffffff" 37 | 38 | return ( 39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
{title}
47 |
48 | Posted by 49 | {author?.username} 50 | 51 | 52 | {toLocaleString(stringToDate(createdAt))} 53 | 54 |
55 |
56 | 57 | {replies.length} 58 | 59 | 60 | {lastReplyAuthor?.username} 61 | 62 | {toLocaleString(stringToDate(lastReply?.createdAt))} 63 | 64 | 65 |
66 | ); 67 | } 68 | export default ThreadComponent; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/deleteHard/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { canUseForum } from "@/services/forum/account/AccountService"; 7 | import { DeleteThread, EditThread, GetThread } from "@/services/forum/thread/ThreadService"; 8 | import { existsSync } from "fs"; 9 | import { rm } from "fs/promises"; 10 | 11 | export async function deleteThreadHard(threadId: string): Promise { 12 | 'use server' 13 | 14 | const session = await getSession(); 15 | if (!session) return "Not logged in"; 16 | const rank = await getHighestRank(session.uuid); 17 | const isStaff = rank?.staff || false; 18 | 19 | if (!isStaff && !(await canUseForum(session.uuid))) return "Permission denied"; 20 | 21 | const res = await DeleteThread(threadId); 22 | 23 | if (isResultError(res)) 24 | return res[2] || res[1].toFixed(); 25 | 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/deleteHard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { deleteThreadHard } from "./ServerActions"; 5 | 6 | interface Props { 7 | params: { 8 | forumId: string; 9 | threadId: string; 10 | }, 11 | } 12 | 13 | export default function Page(props: Props) { 14 | const { forumId, threadId } = props.params; 15 | 16 | const [loading, setLoading] = useState(false); 17 | 18 | function onSubmit() { 19 | setLoading(true); 20 | deleteThreadHard(threadId).then(() => 21 | window.location.assign(`/forums/${forumId}`)) 22 | } 23 | 24 | return
25 |

Delete Thread

26 |
Are you sure you want to delete this threads completely? This action cannot be undone.
27 | 33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/deleteSoft/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { canUseForum } from "@/services/forum/account/AccountService"; 7 | import { DeleteThread, EditThread, GetThread } from "@/services/forum/thread/ThreadService"; 8 | 9 | export async function deleteThreadSoft(threadId: string): Promise { 10 | 'use server' 11 | 12 | const thread = (await GetThread(threadId))[0]; 13 | if (!thread) return "Invalid Thread" 14 | 15 | const session = await getSession(); 16 | if (!session) return "Not logged in"; 17 | const rank = await getHighestRank(session.uuid); 18 | const isStaff = rank?.staff || false; 19 | 20 | if (!isStaff && (thread.author != session.uuid || !(await canUseForum(session.uuid)))) return "Permission denied"; 21 | 22 | thread.body = "The original message was deleted" 23 | const res = await EditThread(thread); 24 | 25 | if (isResultError(res)) 26 | return res[2] || res[1].toFixed(); 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/deleteSoft/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { deleteThreadSoft } from "./ServerActions"; 5 | 6 | interface Props { 7 | params: { 8 | forumId: string; 9 | threadId: string; 10 | }, 11 | } 12 | 13 | export default function Page(props: Props) { 14 | const { forumId, threadId } = props.params; 15 | 16 | const [loading, setLoading] = useState(false); 17 | 18 | function onSubmit() { 19 | setLoading(true); 20 | deleteThreadSoft(threadId).then(() => 21 | window.location.assign(`/forums/${forumId}/${threadId}`)) 22 | } 23 | 24 | return
25 |

Delete Thread Body

26 |
Are you sure you want to delete this threads's text body? This action cannot be undone.
27 | 33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/lock/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { DeleteThread, EditThread, GetThread } from "@/services/forum/thread/ThreadService"; 7 | 8 | export async function lockThread(threadId: string): Promise { 9 | 'use server' 10 | 11 | const thread = (await GetThread(threadId))[0] 12 | if (!thread) return "Thread not found" 13 | 14 | const session = await getSession(); 15 | if (!session) return "Not logged in"; 16 | const rank = await getHighestRank(session.uuid); 17 | const isStaff = rank?.staff || false; 18 | 19 | if (!isStaff) return "Permission denied"; 20 | 21 | thread.locked = true; 22 | const res = await EditThread(thread); 23 | 24 | if (isResultError(res)) 25 | return res[2] || res[1].toFixed(); 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/lock/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { lockThread } from "./ServerActions"; 5 | 6 | interface Props { 7 | params: { 8 | forumId: string; 9 | threadId: string; 10 | }, 11 | } 12 | 13 | export default function Page(props: Props) { 14 | const { forumId, threadId } = props.params; 15 | 16 | const [loading, setLoading] = useState(false); 17 | 18 | function onSubmit() { 19 | setLoading(true); 20 | lockThread(threadId).then(() => 21 | window.location.assign(`/forums/${forumId}/${threadId}`)) 22 | } 23 | 24 | return
25 |

Lock Thread

26 |
Are you sure you want to lock this thread? Only staff members will be able to reply to it.
27 | 33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/styles.css: -------------------------------------------------------------------------------- 1 | [data-theme="dark"] .content-color { 2 | @apply text-gray-400; 3 | } 4 | [data-theme="light"] .content-color { 5 | @apply text-gray-600; 6 | } 7 | 8 | .input:focus-within { 9 | z-index: 2; 10 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/unlock/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { DeleteThread, EditThread, GetThread } from "@/services/forum/thread/ThreadService"; 7 | 8 | export async function unlockThread(threadId: string): Promise { 9 | 'use server' 10 | 11 | const thread = (await GetThread(threadId))[0] 12 | if (!thread) return "Thread not found" 13 | 14 | const session = await getSession(); 15 | if (!session) return "Not logged in"; 16 | const rank = await getHighestRank(session.uuid); 17 | const isStaff = rank?.staff || false; 18 | 19 | if (!isStaff) return "Permission denied"; 20 | 21 | thread.locked = false; 22 | const res = await EditThread(thread); 23 | 24 | if (isResultError(res)) 25 | return res[2] || res[1].toFixed(); 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/[threadId]/unlock/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { unlockThread } from "./ServerActions"; 5 | 6 | interface Props { 7 | params: { 8 | forumId: string; 9 | threadId: string; 10 | }, 11 | } 12 | 13 | export default function Page(props: Props) { 14 | const { forumId, threadId } = props.params; 15 | 16 | const [loading, setLoading] = useState(false); 17 | 18 | function onSubmit() { 19 | setLoading(true); 20 | unlockThread(threadId).then(() => 21 | window.location.assign(`/forums/${forumId}/${threadId}`)) 22 | } 23 | 24 | return
25 |

Lock Thread

26 |
Are you sure you want to unlock this thread? All members will be able to reply to it.
27 | 33 |
34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/[forumId]/page.tsx: -------------------------------------------------------------------------------- 1 | import Table from '@/components/Table/Table'; 2 | import ThreadComponent from './Thread'; 3 | import { GetForumThreads, GetThread } from '@/services/forum/thread/ThreadService'; 4 | import { GetForum } from '@/services/forum/forum/ForumService'; 5 | import { isResultError } from '@/libs/Utils'; 6 | import { getThreadShortId, threadSorter } from '../Utils'; 7 | import Navigation from "@/app/(main)/(withHeaderContent)/forums/(components)/Navigation"; 8 | 9 | interface Params { 10 | params: { 11 | forumId: string; 12 | } 13 | } 14 | export default async function Page({ params: { forumId } }: Params) { 15 | const res0 = await GetForumThreads(forumId); 16 | const isError = isResultError(res0); 17 | if (isError) 18 | console.error("Error while fetching threads from Id: HTTP " + res0[1]); 19 | const threads = res0[0]; 20 | for (let t = 0; t < (threads?.length ?? 0); t++) { 21 | const res = await GetThread(threads![t]._id); 22 | const isError = isResultError(res); 23 | if (isError) 24 | console.error("Error while fetching thread replies: HTTP " + res[1]); 25 | threads![t].replies = res[0]?.replies ?? []; 26 | } 27 | 28 | const res1 = await GetForum(forumId); 29 | if (isResultError(res1)) 30 | console.error("Error while fetching Forum: HTTP " + res1[1]); 31 | const forumDisplayName = res1[0]?.name; 32 | const header = [ 33 | {forumDisplayName}, 34 | Replies, 35 | Latest reply 36 | ]; 37 | const locked = res1[0]?.locked; 38 | return ( 39 | 40 |
42 | 43 | {locked 44 | ? This Forum is Locked 45 | : <>} 46 | {!threads || !threads.length ? 47 | {isError ? 48 |

Error while fetching threads for forum.

: 49 | 0 threads in this forum. Be the first!}
: 50 | threads.sort(threadSorter).map(t => 51 | 54 | )} 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createCategory/CreateCategoryForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { createCategory } from "./ServerActions"; 5 | 6 | export default function CreateCategoryForm() { 7 | const [loading, setLoading] = useState(); 8 | const [error, setError] = useState(); 9 | 10 | function onSubmit(formData: FormData) { 11 | setLoading(true); 12 | 13 | createCategory(formData).then(res => { 14 | if (res) { 15 | setLoading(false); 16 | setError(res); 17 | } else { 18 | setLoading(true); 19 | window.location.assign("/forums"); 20 | } 21 | }) 22 | } 23 | 24 | return
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | {error} 35 |
36 | 42 |
43 | } 44 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createCategory/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { CreateForumCategory } from "@/services/forum/category/CategoryService"; 7 | 8 | export async function createCategory(formData: FormData): Promise { 9 | 10 | const session = await getSession(); 11 | if (!session) return "Not logged in"; 12 | const rank = await getHighestRank(session.uuid); 13 | const isStaff = rank?.staff || false; 14 | 15 | if (!isStaff) return "Permission denied"; 16 | 17 | const nameEntry = formData.get("name"); 18 | const weightEntry = formData.get("weight"); 19 | 20 | if (!nameEntry) 21 | return "Name cannot be empty" 22 | 23 | if (!weightEntry) 24 | return "Weight cannot be empty" 25 | 26 | const name = nameEntry.toString(); 27 | const weight = Number(weightEntry.toString()); 28 | const id = newUuid(); 29 | 30 | const res = await CreateForumCategory(id, name, weight); 31 | 32 | if (isResultError(res)) 33 | return res[2] || res[1].toFixed(); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createCategory/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import CreateCategoryForm from "./CreateCategoryForm"; 5 | 6 | 7 | export default async function Page() { 8 | const session = await getSession(); 9 | const rank = await getHighestRank(session.uuid); 10 | if (!rank?.staff) { 11 | redirect("/forums"); 12 | } 13 | 14 | return
15 |

Create Category

16 |
17 | 18 |
19 | } 20 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createForum/CreateForumForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { createForum } from "./ServerActions"; 5 | import ForumCategory from "@/libs/types/entities/ForumCategory"; 6 | 7 | export interface Props { 8 | categories: ForumCategory[] 9 | } 10 | 11 | export default function CreateForumForm(props: Props) { 12 | const { categories } = props; 13 | 14 | const [loading, setLoading] = useState(); 15 | const [error, setError] = useState(); 16 | 17 | function onSubmit(formData: FormData) { 18 | setLoading(true); 19 | 20 | createForum(formData).then(res => { 21 | if (res) { 22 | setLoading(false); 23 | setError(res); 24 | } else { 25 | setLoading(true); 26 | window.location.assign("/forums"); 27 | } 28 | }) 29 | } 30 | 31 | return
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 |
48 |
49 | 50 | 56 |
57 |
58 | {error} 59 |
60 | 66 |
67 | } 68 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createForum/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import Forum from "@/libs/types/entities/Forum"; 6 | import { getHighestRank } from "@/services/controller/GrantService"; 7 | import { CreateForumCategory, GetForumCategories } from "@/services/forum/category/CategoryService"; 8 | import { CreateForum } from "@/services/forum/forum/ForumService"; 9 | 10 | export async function createForum(formData: FormData): Promise { 11 | 12 | const session = await getSession(); 13 | if (!session) return "Not logged in"; 14 | const rank = await getHighestRank(session.uuid); 15 | const isStaff = rank?.staff || false; 16 | 17 | if (!isStaff) return "Permission denied"; 18 | 19 | const nameEntry = formData.get("name"); 20 | const descEntry = formData.get("description"); 21 | const weightEntry = formData.get("weight"); 22 | const locked = formData.get("locked")?.toString() == "on" || false; 23 | const categoryEntry = formData.get("category"); 24 | 25 | if (!nameEntry) 26 | return "Name cannot be empty" 27 | 28 | if (!weightEntry) 29 | return "Weight cannot be empty" 30 | 31 | if (!descEntry) 32 | return "Description cannot be empty" 33 | 34 | if (!categoryEntry) 35 | return "Category is not selected" 36 | 37 | const categoryId = categoryEntry.toString(); 38 | 39 | const category = ((await GetForumCategories())[0] || []).find(c => c._id == categoryId)!; 40 | 41 | const req: Forum = { 42 | _id: newUuid(), 43 | name: nameEntry.toString(), 44 | description: descEntry.toString(), 45 | weight: Number(weightEntry.toString()), 46 | locked: locked, 47 | category: categoryId, 48 | categoryName: category.name, 49 | categoryWeight: category.weight, 50 | threadAmount: 0, 51 | } 52 | 53 | const res = await CreateForum(req); 54 | 55 | if (isResultError(res)) 56 | return res[2] || res[1].toFixed(); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/createForum/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import { GetForumCategories } from "@/services/forum/category/CategoryService"; 5 | import CreateForumForm from "./CreateForumForm"; 6 | 7 | export default async function Page() { 8 | const session = await getSession(); 9 | const rank = await getHighestRank(session.uuid); 10 | if (!rank?.staff) { 11 | redirect("/forums"); 12 | } 13 | 14 | const categories = (await GetForumCategories())[0] || []; 15 | 16 | return
17 |

Create Forum

18 |
19 | 20 |
21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/deleteCategory/DeleteCategoryForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from "react"; 4 | import { deleteCategory } from "./ServerActions"; 5 | import ForumCategory from "@/libs/types/entities/ForumCategory"; 6 | 7 | export interface Props { 8 | categories: ForumCategory[] 9 | } 10 | 11 | export default function DeleteCategoryForm(props: Props) { 12 | const { categories } = props; 13 | 14 | const [loading, setLoading] = useState(); 15 | const [error, setError] = useState(); 16 | 17 | function onSubmit(formData: FormData) { 18 | setLoading(true); 19 | 20 | const category = categories.find(c => c._id == formData.get("id")?.toString() || ""); 21 | 22 | const result = confirm(`Are you sure you want to delete ${category?.name || "null"}?`) 23 | 24 | if (!result) { 25 | setLoading(false); 26 | return; 27 | } 28 | 29 | deleteCategory(formData).then(res => { 30 | if (res) { 31 | setLoading(false); 32 | setError(res); 33 | } else { 34 | setLoading(true); 35 | window.location.assign("/forums"); 36 | } 37 | }) 38 | } 39 | 40 | return
41 |
42 | 43 | 49 |
50 |
51 | {error} 52 |
53 | 59 |
60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/deleteCategory/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { CreateForumCategory, DeleteForumCategory, GetForumCategories } from "@/services/forum/category/CategoryService"; 7 | import { DeleteThread, GetForumThreads } from "@/services/forum/thread/ThreadService"; 8 | 9 | export async function deleteCategory(formData: FormData): Promise { 10 | 11 | const session = await getSession(); 12 | if (!session) return "Not logged in"; 13 | const rank = await getHighestRank(session.uuid); 14 | const isStaff = rank?.staff || false; 15 | 16 | if (!isStaff) return "Permission denied"; 17 | 18 | const id = formData.get("id"); 19 | 20 | if (!id) 21 | return "Category not selected" 22 | 23 | const categories = (await GetForumCategories())[0] || []; 24 | const category = categories.find(c => c._id = id.toString()); 25 | if (!category) 26 | return "Category not found" 27 | 28 | const forums = category.forums; 29 | 30 | for (let f of forums) { 31 | const threads = (await GetForumThreads(f._id))[0] || [] 32 | 33 | for(let t of threads) { 34 | await DeleteThread(t._id); 35 | } 36 | 37 | } 38 | 39 | const res = await DeleteForumCategory(id.toString()); 40 | 41 | if (isResultError(res)) 42 | return res[2] || res[1].toFixed(); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/deleteCategory/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import DeleteCategoryForm from "./DeleteCategoryForm"; 5 | import { GetForumCategories } from "@/services/forum/category/CategoryService"; 6 | 7 | 8 | export default async function Page() { 9 | const session = await getSession(); 10 | const rank = await getHighestRank(session.uuid); 11 | if (!rank?.staff) { 12 | redirect("/forums"); 13 | } 14 | 15 | const categories = (await GetForumCategories())[0] || []; 16 | 17 | return
18 |

Delete Category

19 |
20 | 21 |
22 | } 23 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/deleteForum/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { DeleteForum } from "@/services/forum/forum/ForumService"; 7 | import { DeleteThread, GetForumThreads } from "@/services/forum/thread/ThreadService"; 8 | 9 | export async function deleteForum(formData: FormData): Promise { 10 | 11 | const session = await getSession(); 12 | if (!session) return "Not logged in"; 13 | const rank = await getHighestRank(session.uuid); 14 | const isStaff = rank?.staff || false; 15 | 16 | if (!isStaff) return "Permission denied"; 17 | 18 | const id = formData.get("forumId"); 19 | 20 | if (!id) 21 | return "Forum not selected" 22 | 23 | const threads = (await GetForumThreads(id.toString()))[0] || [] 24 | 25 | for(let t of threads) { 26 | await DeleteThread(t._id); 27 | } 28 | 29 | const res = await DeleteForum(id.toString()); 30 | 31 | if (isResultError(res)) 32 | return res[2] || res[1].toFixed(); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/deleteForum/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import { GetForumCategories } from "@/services/forum/category/CategoryService"; 5 | import DeleteForumForm from "./DeleteForumForm"; 6 | 7 | 8 | export default async function Page() { 9 | const session = await getSession(); 10 | const rank = await getHighestRank(session.uuid); 11 | if (!rank?.staff) { 12 | redirect("/forums"); 13 | } 14 | 15 | const categories = (await GetForumCategories())[0] || []; 16 | 17 | return
18 |

Delete Forum

19 |
20 | 21 |
22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/layout.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from '@/components/HeaderContext'; 2 | import './styles.css' 3 | 4 | export const revalidate = 5; 5 | export default function Layout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | const headerContent: [string, string] = ["Forums", `See what's going on and interact with the community!`]; 11 | return <> 12 | 13 |
{children}
14 | ; 15 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/lockForum/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { DeleteForum, EditForum, GetForum } from "@/services/forum/forum/ForumService"; 7 | 8 | export async function lockForum(formData: FormData): Promise { 9 | 10 | const session = await getSession(); 11 | if (!session) 12 | return "Not logged in"; 13 | const rank = await getHighestRank(session.uuid); 14 | const isStaff = rank?.staff || false; 15 | if (!isStaff) 16 | return "Permission denied"; 17 | 18 | const id = formData.get("forumId"); 19 | if (!id) 20 | return "Forum not selected" 21 | 22 | const forum = (await GetForum(id.toString()))[0] 23 | if (!forum) 24 | return "Forum not found" 25 | 26 | forum.locked = true 27 | const res = await EditForum(forum); 28 | 29 | if (isResultError(res)) 30 | return res[2] || res[1].toFixed(); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/lockForum/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import { GetForumCategories } from "@/services/forum/category/CategoryService"; 5 | import LockForumForm from "./LockForumForm"; 6 | 7 | 8 | export default async function Page() { 9 | const session = await getSession(); 10 | const rank = await getHighestRank(session.uuid); 11 | if (!rank?.staff) { 12 | redirect("/forums"); 13 | } 14 | 15 | const categories = (await GetForumCategories())[0] || []; 16 | 17 | return
18 |

Lock Forum

19 |
20 | 21 |
22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/styles.css: -------------------------------------------------------------------------------- 1 | .aside-container { 2 | display: grid; 3 | flex: 1 1 fit-content; 4 | justify-content: center; 5 | --col-sizes: 193px; 6 | grid-template-columns: repeat(auto-fit, minmax(var(--col-sizes), 1fr)); 7 | grid-auto-flow: row dense; 8 | } 9 | @media (max-width: 434px) { 10 | .aside-container { 11 | --col-sizes: 100%; 12 | place-content: stretch; 13 | } 14 | } 15 | 16 | .categories { 17 | max-width: 996px; 18 | flex: 1 0 fit-content; 19 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/unlockForum/ServerActions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { isResultError, newUuid } from "@/libs/Utils"; 4 | import getSession from "@/libs/session/getSession"; 5 | import { getHighestRank } from "@/services/controller/GrantService"; 6 | import { DeleteForum, EditForum, GetForum } from "@/services/forum/forum/ForumService"; 7 | 8 | export async function unlockForum(formData: FormData): Promise { 9 | 10 | const session = await getSession(); 11 | if (!session) 12 | return "Not logged in"; 13 | const rank = await getHighestRank(session.uuid); 14 | const isStaff = rank?.staff || false; 15 | if (!isStaff) 16 | return "Permission denied"; 17 | 18 | const id = formData.get("forumId"); 19 | if (!id) 20 | return "Forum not selected" 21 | 22 | const forum = (await GetForum(id.toString()))[0] 23 | if (!forum) 24 | return "Forum not found" 25 | 26 | forum.locked = false 27 | const res = await EditForum(forum); 28 | 29 | if (isResultError(res)) 30 | return res[2] || res[1].toFixed(); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/forums/unlockForum/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { getHighestRank } from "@/services/controller/GrantService"; 3 | import { redirect } from "next/navigation"; 4 | import { GetForumCategories } from "@/services/forum/category/CategoryService"; 5 | import UnlockForumForm from "./UnlockForumForm"; 6 | 7 | export default async function Page() { 8 | const session = await getSession(); 9 | const rank = await getHighestRank(session.uuid); 10 | if (!rank?.staff) { 11 | redirect("/forums"); 12 | } 13 | 14 | const categories = (await GetForumCategories())[0] || []; 15 | 16 | return
17 |

Unlock Forum

18 |
19 | 20 |
21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/layout.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css'; 2 | import HeaderContent from './HeaderContent' 3 | import Header from '../(layout-components)/Header'; 4 | import RouteSegmentNav from "@/app/(main)/(withHeaderContent)/forums/(components)/RouteSegmentNav"; 5 | import getSession from '@/libs/session/getSession'; 6 | import { getHighestRank } from '@/services/controller/GrantService'; 7 | 8 | export default async function Layout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | const session = await getSession(); 14 | const isStaff = (await getHighestRank(session.uuid))?.staff || false; 15 | 16 | return <> 17 |
18 | 19 |
20 |
21 | {children} 22 |
23 |
24 | ; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/staff/page.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from "@/components/HeaderContext"; 2 | import { ServerMCHead } from "@/components/Minecraft/Server"; 3 | import { GetRank, getRankColor } from "@/services/controller/GrantService"; 4 | import { getStaffUsers, getUsernameFromUuid } from "@/services/forum/account/AccountService"; 5 | import Link from "next/link"; 6 | import React from "react"; 7 | 8 | export default async function Staff() { 9 | const staffUuids = (await getStaffUsers())[0]!; 10 | 11 | let staff = []; 12 | let aux = []; 13 | let crr = ""; 14 | 15 | for (let entry of staffUuids) { 16 | if (crr != entry.rankUuid) { 17 | if (aux.length > 0) 18 | staff.push(aux); 19 | 20 | crr = entry.rankUuid; 21 | aux = []; 22 | } 23 | 24 | const rank = (await GetRank(entry.rankUuid))[0]!; 25 | aux.push({ 26 | username: (await getUsernameFromUuid(entry.playerUuid))!, 27 | rank: rank.name, 28 | color: await getRankColor(entry.rankUuid) || "#FFFFFF" 29 | }) 30 | } 31 | 32 | if (aux.length > 0) 33 | staff.push(aux); 34 | 35 | const headerContent: [string, string] = ["Staff", `Running the show!`]; 36 | return <> 37 | 38 | 39 | {staff.map((listPerRank, j) => ( 40 |
41 |

{listPerRank[0].rank}

42 |
43 | {listPerRank.map((d, i) => ( 44 |
45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 |
{d.username}
53 | {d.rank} 54 |
55 | 56 |
57 | ))} 58 |
59 |
60 | ))} 61 | ; 62 | } 63 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/styles.css: -------------------------------------------------------------------------------- 1 | /* #region HeaderContent */ 2 | .header-content-bg { 3 | background-image: url('/img/header-bg.png'); 4 | } 5 | 6 | .text-stroke { 7 | --thickness: .4px; 8 | --filter: drop-shadow(0px 0px var(--thickness) rgba(0,0,0,1)); 9 | filter: var(--filter) var(--filter) var(--filter) var(--filter); 10 | } 11 | /* #endregion */ 12 | 13 | .sect { 14 | --h-diff: 13rem; 15 | min-height: calc(min(1920px, 100vh) - var(--h-diff)); 16 | width: 100% 17 | } 18 | 19 | .window { 20 | max-width: 1280px; 21 | } 22 | @media (max-width: 1280px) { 23 | .window { 24 | max-width: 1024px; 25 | } 26 | } 27 | @media (max-width: 1024px) { 28 | .window { 29 | max-width: 768px; 30 | } 31 | } 32 | @media (max-width: 768px) { 33 | .window { 34 | max-width: 640px; 35 | } 36 | } 37 | @media (max-width: 640px) { 38 | .window { 39 | flex-direction: column; 40 | width: 100%; 41 | } 42 | .window .content { 43 | width: auto; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/CreateTicket/styles.css: -------------------------------------------------------------------------------- 1 | 2 | .text-error { 3 | white-space: pre-wrap; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import RouteSegmentNav from './RouteSegmentNav'; 2 | import {headers} from "next/headers"; 3 | 4 | export default function Navigation({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return <> 10 |
11 |
12 | 13 |
14 | {children} 15 |
16 |
17 |
18 | ; 19 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import TableHeader from './TableHeader'; 2 | import './styles.css' 3 | 4 | export interface TableData { 5 | headerContent: JSX.Element[]; 6 | children: React.ReactNode; 7 | } 8 | const Table = (props: TableData) => { 9 | const { 10 | headerContent, 11 | children 12 | } = props; 13 | 14 | return ( 15 |
16 | {headerContent} 17 | {children} 18 |
19 | ); 20 | } 21 | export default Table; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/Table/TableEntry.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | export interface TableEntryData { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | const TableEntry = (props: TableEntryData) => { 8 | const { 9 | children, 10 | className 11 | } = props; 12 | 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | } 19 | export default TableEntry; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/Table/TableHeader.tsx: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | 3 | export interface HeaderData { 4 | children: React.ReactNode; 5 | } 6 | const Header = (props: HeaderData) => { 7 | const { 8 | children 9 | } = props; 10 | 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | } 17 | export default Header; -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/Table/styles.css: -------------------------------------------------------------------------------- 1 | .table-grid { 2 | display: grid; 3 | align-items: stretch; 4 | justify-content: stretch; 5 | align-content: space-between; 6 | grid-template-columns: repeat(auto-fit, minmax(122px, 1fr)); 7 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/(components)/TicketComponent.tsx: -------------------------------------------------------------------------------- 1 | import TableEntry from './/Table/TableEntry'; 2 | import NavLink from '@/components/NavLink/component'; 3 | import { stringToDate, toLocaleString } from '@/libs/Utils'; 4 | import { getAuthorInfo } from '../Utils'; 5 | 6 | export interface ThreadData { 7 | id: string; 8 | authorId: string; // UUID 9 | createdAt: string; 10 | lastUpdatedAt: string; 11 | category: string; 12 | title: string; 13 | status: string; 14 | classname?: string; 15 | } 16 | const TicketComponent = async (props: ThreadData) => { 17 | const { 18 | id, 19 | authorId, 20 | createdAt, 21 | lastUpdatedAt, 22 | category, 23 | title, 24 | status, 25 | classname 26 | } = props; 27 | const author = await getAuthorInfo(authorId); 28 | 29 | const getRankColor = (r?: string) => ({ // TODO: Properly get rank color 30 | Owner: "#9F000C", 31 | Developer: "#ff4141" 32 | }[r ?? '']) ?? "#ffffff" 33 | 34 | return ( 35 | 36 | {status} 37 | 38 | 39 | {title} 40 | 41 | 42 | {category} 43 | {toLocaleString(stringToDate(lastUpdatedAt))} 44 | {toLocaleString(stringToDate(createdAt))} 45 | 46 | 47 | 48 | {author?.username} 50 | 51 | 52 | ); 53 | } 54 | export default TicketComponent; 55 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/Utils.ts: -------------------------------------------------------------------------------- 1 | import { GetProfileFromUuid } from '@/services/controller/ProfileService'; 2 | import { GetActiveRanks } from '@/services/controller/GrantService'; 3 | import Rank from '@/libs/types/entities/Rank'; 4 | import { isResultError, tryParseInt } from '@/libs/Utils'; 5 | import Thread from '@/libs/types/entities/Thread'; 6 | 7 | export const getAuthorInfo = async (authorId?: string) => { 8 | if (!authorId) return; 9 | 10 | const author: {username: string, rank?: Rank} = {username: '', rank: undefined}; 11 | const profilePromise = GetProfileFromUuid(authorId) 12 | .then(res => { 13 | if (!isResultError(res)) return res; 14 | console.error("Error fetching author: HTTP " + res[1]); 15 | }); 16 | const ranksPromise = GetActiveRanks(authorId) 17 | .then(res => { 18 | if (!isResultError(res)) return res; 19 | console.error("Error fetching author rank: HTTP " + res[1]); 20 | }); 21 | 22 | const profile = await profilePromise; 23 | if (!profile) return; 24 | author.username = profile[0]!.name; 25 | 26 | const ranks = await ranksPromise; 27 | if (ranks) 28 | author.rank = ranks[0]!.sort((a, b) => a.priority - b.priority)[ranks[0]!.length - 1]; 29 | 30 | return author; 31 | }; 32 | 33 | export const threadSorter = (a?: { createdAt: string }, b?: { createdAt: string }) => { 34 | const intA = tryParseInt(a?.createdAt); 35 | const intB = tryParseInt(b?.createdAt); 36 | return intA && intB ? intB - intA : 0; 37 | }; 38 | 39 | export const getThreadShortId = (id?: string) => { 40 | const temp = id?.split('.'); 41 | return temp?.[temp.length - 1]; 42 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/layout.tsx: -------------------------------------------------------------------------------- 1 | import HeaderContext from '@/components/HeaderContext'; 2 | import './styles.css' 3 | 4 | export const revalidate = 5; 5 | export default function Layout({ 6 | children, 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | const headerContent: [string, string] = ["Tickets", `Here's where we help you`]; 11 | return <> 12 | 13 |
{children}
14 | ; 15 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/page.tsx: -------------------------------------------------------------------------------- 1 | import getSession from "@/libs/session/getSession"; 2 | import { redirect } from "next/navigation"; 3 | 4 | export default async function Page() { 5 | const session = await getSession(); 6 | 7 | if (!session.isLoggedIn) { 8 | return redirect("/auth/login"); 9 | } 10 | 11 | redirect('/support/1'); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/styles.css: -------------------------------------------------------------------------------- 1 | .aside-container { 2 | display: grid; 3 | flex: 1 1 fit-content; 4 | justify-content: center; 5 | --col-sizes: 193px; 6 | grid-template-columns: repeat(auto-fit, minmax(var(--col-sizes), 1fr)); 7 | grid-auto-flow: row dense; 8 | } 9 | @media (max-width: 434px) { 10 | .aside-container { 11 | --col-sizes: 100%; 12 | place-content: stretch; 13 | } 14 | } 15 | 16 | .categories { 17 | max-width: 996px; 18 | flex: 1 0 fit-content; 19 | } -------------------------------------------------------------------------------- /src/app/(main)/(withHeaderContent)/support/tickets/[ticketId]/ReplyForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import './styles.css' 4 | import useSession from "@/hooks/useSession"; 5 | import { useEffect, useRef, useState } from "react"; 6 | import Ticket from '@/libs/types/entities/Ticket'; 7 | import TicketCategory from '@/libs/types/entities/TicketCategory'; 8 | import { reply } from './TicketServerActions'; 9 | 10 | export interface WriteReplyData { 11 | parentTicket: Ticket; 12 | categories: TicketCategory[] 13 | } 14 | const WriteReply = (props: WriteReplyData) => { 15 | const { parentTicket, categories } = props 16 | const [isLoading, setIsLoading] = useState(false); 17 | const [error, setError] = useState(null); 18 | const { session } = useSession(); 19 | const input = useRef(null); 20 | const hiddenDiv = useRef(null); 21 | 22 | async function onSubmit(formData: FormData) { 23 | setIsLoading(true); 24 | 25 | reply(parentTicket, formData).then(res => { 26 | if (res) { 27 | setIsLoading(false) 28 | setError(res) 29 | } else { 30 | setError("") 31 | window.location.reload(); 32 | } 33 | }); 34 | } 35 | 36 | useEffect(() => { 37 | input.current!.oninput = () => { 38 | hiddenDiv.current!.innerHTML = input.current!.value + "\n\n"; 39 | hiddenDiv.current!.style.display = "block"; 40 | hiddenDiv.current!.hidden = true; 41 | input.current!.style.height = hiddenDiv.current!.offsetHeight + "px"; 42 | hiddenDiv.current!.style.display = "none"; 43 | hiddenDiv.current!.hidden = false; 44 | } 45 | }, []) 46 | 47 | const isDisabled = parentTicket.status != "open"; 48 | 49 | return <> 50 |
51 |
52 | {error &&

{error}

}
53 |