├── .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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
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 |
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 |
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
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
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
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 |
66 | >;
67 | }
68 | export default WriteReply;
69 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/support/tickets/[ticketId]/TicketActions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Ticket from "@/libs/types/entities/Ticket"
4 | import { ArchiveTicket, UnarchiveTicket } from "@/services/forum/ticket/TicketService";
5 | import Rank from "@/libs/types/entities/Rank";
6 | import { closeTicket, openTicket, setResult } from "./TicketServerActions";
7 |
8 | export interface TicketActionProps {
9 | ticket: Ticket,
10 | userRank: Rank | undefined,
11 | results: {name:string, color:string}[],
12 | }
13 |
14 | export default function TicketActions(props: TicketActionProps) {
15 | let { ticket, userRank, results } = props;
16 |
17 | return
18 | {ticket.status != "archived"
19 | ? <>
20 | {ticket.status == "closed"
21 | ?
24 | : }
27 | {userRank?.staff
28 | ? <>
31 | {results.map((r,i) =>
32 |
35 | )}>
36 | : <>>}
37 | >
38 | : userRank?.staff
39 | ? <>
42 | {results.map((r,i) =>
43 |
46 | )}>
47 | : <>>}
48 |
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/support/tickets/[ticketId]/TicketServerActions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { isResultError, newUuid } from "@/libs/Utils";
4 | import getSession from "@/libs/session/getSession";
5 | import Ticket from "@/libs/types/entities/Ticket";
6 | import { getAllFilters } from "@/services/forum/filter/TextFilterService";
7 | import { CreateReplyTicket, EditTicket } from "@/services/forum/ticket/TicketService";
8 | import { getAuthorInfo } from "../../Utils";
9 |
10 |
11 | export async function closeTicket(ticket: Ticket) {
12 | 'use server'
13 | ticket.status = "closed";
14 | return await EditTicket(ticket);
15 | }
16 |
17 | export async function openTicket(ticket: Ticket) {
18 | 'use server'
19 | ticket.status = "open";
20 | return await EditTicket(ticket);
21 | }
22 |
23 | export async function setResult(ticket: Ticket, result: string) {
24 | 'use server'
25 | ticket.result = result;
26 | return await EditTicket(ticket);
27 | }
28 |
29 | export async function reply(parent: Ticket, formData: FormData) {
30 | 'use server'
31 | const session = await getSession();
32 |
33 | const author = session?.uuid
34 | if (!author)
35 | return "You're not logged in."
36 | const currentUser = await getAuthorInfo(session.uuid);
37 | if (!currentUser?.rank?.staff && parent.author != session.uuid)
38 | return "Permission Denied"
39 |
40 | const bodyEntry = formData.get("reply")
41 | if (!bodyEntry)
42 | return "Please write a reply."
43 |
44 | const body = bodyEntry.toString();
45 |
46 | const filters = (await getAllFilters())[0] || [];
47 | for (let filter of filters) {
48 | if (body.includes(filter.filter))
49 | return "Message did not pass filter test"
50 | }
51 |
52 | console.log(filters);
53 |
54 | const id = newUuid();
55 |
56 | const reply: Ticket = {
57 | _id: id,
58 | author: author,
59 | body: body,
60 | category: "",
61 | createdAt: Date.now().toFixed(),
62 | lastUpdatedAt: Date.now().toFixed(),
63 | parentTicket: parent._id,
64 | status: "Sent",
65 | result: "",
66 | title: `Reply to ${parent._id}`,
67 | replies: [],
68 | }
69 |
70 | const res = await CreateReplyTicket(reply)
71 |
72 | if (isResultError(res))
73 | return res[2] || res[1].toFixed();
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/support/tickets/[ticketId]/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 | }
11 |
12 | textarea {
13 | overflow: hidden;
14 | font-size: 1rem;
15 | }
16 |
17 | #replyhiddendiv {
18 | display: none;
19 | white-space: pre-wrap;
20 | word-wrap: break-word;
21 | font-size: 1rem;
22 | }
23 |
24 | .ticket-content {
25 | white-space: pre-line;
26 | word-wrap: break-word;
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/SocialConnections.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import ClipboardTooltip from "@/components/ClipboardTooltip/ClipboardTooltip";
4 | import Link from "next/link";
5 | import { useRef } from "react";
6 | import { FaDiscord, FaInstagram, FaTwitter, FaYoutube } from "react-icons/fa6";
7 |
8 | export interface Props {
9 | discord: string | undefined,
10 | instagram: string | undefined,
11 | youtube: string | undefined,
12 | twitter: string | undefined,
13 | }
14 |
15 | export default function SocialConnections(props: Props) {
16 | const { discord, instagram, youtube, twitter } = props;
17 |
18 | return
19 |
Social Media
20 |
21 | {instagram ?
Instagram : <>>}
22 | {twitter ?
Twitter : <>>}
23 | {youtube ?
YouTube : <>>}
24 | {discord ?
25 | {discord}
26 |
27 | : <>>}
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/forums/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | interface Params {
3 | params: {
4 | username: string
5 | }
6 | }
7 | export default function Page({ params: { username } }: Params) {
8 | return <>Forums>
9 | }
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/general/StatsWidget.tsx:
--------------------------------------------------------------------------------
1 |
2 | export interface Props {
3 | title: string,
4 | entries: string[],
5 | backgroundImage: string,
6 | }
7 |
8 | export default async function StatsWidget(props: Props) {
9 | const { title, entries, backgroundImage } = props;
10 | const widgetStyle = {
11 | backgroundColor: "#505050",
12 | backgroundImage: `url(${backgroundImage})`,
13 | backgroundBlendMode: "lighten",
14 | }
15 |
16 | return <>
17 |
18 |
19 |
{title}
20 |
21 | {entries.map(entry =>
22 | {entry}
23 | )}
24 |
25 |
26 |
27 | >
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/general/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUuid } from "@/services/forum/account/AccountService";
2 | import { getSkywarsStats } from "@/services/forum/stats/GameStatsService";
3 | import StatsWidget from "./StatsWidget";
4 | import { SKYWARS_NICENAME } from "@/libs/types/entities/SkywarsStats";
5 |
6 | interface Params {
7 | params: {
8 | username: string
9 | }
10 | }
11 |
12 | export default async function Page(parameters: Params) {
13 | const { params: { username } } = parameters;
14 | const uuid = await getUuid(username);
15 |
16 | const skywarsStats = (await getSkywarsStats(uuid))[0]!;
17 | const skywarsStatsArr = Object.keys(skywarsStats).reduce(
18 | (acc, key) =>
19 | { acc.push(`${(SKYWARS_NICENAME as any)[key]}: ${(skywarsStats as any)[key]}`); return acc },
20 | []);
21 |
22 | return
23 |
24 |
25 |
26 |
;
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/page.tsx:
--------------------------------------------------------------------------------
1 | import { permanentRedirect } from 'next/navigation';
2 |
3 | interface Params {
4 | params: {
5 | username: string
6 | }
7 | }
8 | export default function Redirect({ params: { username } }: Params) {
9 | permanentRedirect(`/u/${username}/general`);
10 | }
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/staff/identity/page.tsx:
--------------------------------------------------------------------------------
1 | import { MCHead } from "@/components/Minecraft/base";
2 | import { GetProfileFromUuid } from "@/services/controller/ProfileService";
3 | import { getAccountFromUuid, getUsernameFromUuid, getUuid } from "@/services/forum/account/AccountService";
4 |
5 | interface Params {
6 | params: {
7 | username: string
8 | }
9 | }
10 |
11 | export default async function Page(params: Params) {
12 | const { username } = params.params;
13 |
14 | const uuid = await getUuid(username);
15 | const profile = (await GetProfileFromUuid(uuid))[0]!;
16 | const account = (await getAccountFromUuid(uuid))[0];
17 |
18 | const altName: { [key: string]: string } = {};
19 |
20 | for (let alt of profile.alts) {
21 | const name = await getUsernameFromUuid(alt);
22 | if (!name) continue;
23 |
24 | altName[alt] = name;
25 | }
26 |
27 | return
28 |
29 |
30 | Forum Account Email
31 |
32 |
33 | {account?.email || "Not registered"}
34 |
35 |
36 |
37 |
38 | Alts
39 |
40 |
41 | {profile.alts.map((uuid, i) =>
42 |
43 |
44 |
{altName[uuid]}
45 |
46 | )}
47 |
48 |
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/staff/layout.tsx:
--------------------------------------------------------------------------------
1 | import NavLink from "@/components/NavLink/component";
2 | import getSession from "@/libs/session/getSession"
3 | import { GetActiveRanks } from "@/services/controller/GrantService";
4 | import { headers } from "next/headers";
5 | import React from "react";
6 | import "./styles.css"
7 |
8 | interface UserParams {
9 | children: React.ReactNode;
10 | }
11 | export default async function Layout({ children: children }: UserParams) {
12 | const currPath = new URL(headers().get('x-url')!).pathname;
13 | const username = currPath?.split('/')[2];
14 | const session = await getSession();
15 | const sessionRanks = (await GetActiveRanks(session.uuid))[0]!;
16 | const sessionIsStaff = sessionRanks.find(rank => rank.staff) != undefined;
17 | if (!sessionIsStaff) {
18 | return <>Forbidden>;
19 | }
20 |
21 | return
22 |
23 |
24 | Punishments
25 |
26 |
27 | Grants
28 |
29 |
30 | Identity
31 |
32 |
33 |
34 | {React.Children.map(children, child =>
35 | React.isValidElement(child) ? React.cloneElement(child, {} as any) : child
36 | )}
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/staff/page.tsx:
--------------------------------------------------------------------------------
1 | import { permanentRedirect } from 'next/navigation';
2 |
3 | interface Params {
4 | params: {
5 | username: string
6 | }
7 | }
8 | export default function Redirect({ params: { username } }: Params) {
9 | permanentRedirect(`/u/${username}/staff/punishments`);
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/staff/punishments/ServerActions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getUsernameFromUuid, getUuid } from "@/services/forum/account/AccountService"
4 | import { getPunishments } from "@/services/forum/punishment/PunishmentService";
5 |
6 | export async function getPunishmentsInfo(username: string) {
7 | const uuid = (await getUuid(username));
8 |
9 | const punishments = (await getPunishments(uuid))[0] || [];
10 |
11 | const uuidToName: { [key: string]: string } = {};
12 | uuidToName["00000000-0000-0000-0000-000000000000"] = "Server/Console"
13 |
14 | for (let p of punishments) {
15 | for (let proof of p.proof) {
16 | if (!uuidToName[proof.addedBy])
17 | uuidToName[proof.addedBy] = (await getUsernameFromUuid(proof.addedBy))!;
18 | }
19 |
20 | if (!uuidToName[p.issuedBy])
21 | uuidToName[p.issuedBy] = (await getUsernameFromUuid(p.issuedBy))!;
22 |
23 | if (!uuidToName[p.removedBy])
24 | uuidToName[p.removedBy] = (await getUsernameFromUuid(p.removedBy))!;
25 | }
26 |
27 | punishments.sort((a,b) => Number(b.issuedAt) - Number(a.issuedAt));
28 |
29 | return {
30 | uuid: uuid,
31 | uuidToName: uuidToName,
32 | punishments: punishments,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/staff/styles.css:
--------------------------------------------------------------------------------
1 |
2 | .btn.active {
3 | background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
4 | font-weight: bolder;
5 | }
6 |
7 | .btn:hover {
8 | border-color: unset;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/statistics/page.tsx:
--------------------------------------------------------------------------------
1 | interface Params {
2 | params: {
3 | username: string
4 | }
5 | }
6 | export default function Page({ params: { username } }: Params) {
7 | return <>Punishments>
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/[username]/styles.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 640px) {
2 | .content .inner-content {
3 | height: auto;
4 | }
5 | }
6 |
7 | .grid-container {
8 | display: grid;
9 | flex: 0 0 min-content;
10 |
11 | justify-content: center;
12 |
13 | grid-template-columns: repeat(auto-fit, minmax(193px, 1fr));
14 | grid-auto-flow: row dense;
15 |
16 | align-items: start;
17 |
18 | row-gap: 1rem;
19 | }
20 |
21 | .grid-container .text-shadow {
22 | filter: drop-shadow(-.75px 1px .5px #848484);
23 | }
24 |
25 | #discord-link:hover {
26 | cursor:pointer;
27 | }
28 |
29 | #discord-tooltip-spike {
30 | width: 0;
31 | height: 0;
32 | border-left: 10px solid transparent;
33 | border-right: 10px solid transparent;
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/(main)/(withHeaderContent)/u/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('/' + (session?.isLoggedIn ? `u/${session.username}` : ''));
7 | }
--------------------------------------------------------------------------------
/src/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 | import MobileNavbar from '@/components/MobileNavbar/component';
3 | import MobileNavbarContent from './(layout-components)/MobileNavbarContent';
4 | import Footer from './(layout-components)/Footer';
5 |
6 | export default async function Layout({
7 | children,
8 | }: {
9 | children: React.ReactNode
10 | }) {
11 | return <>
12 | }
15 | />
16 |
17 | {children}
18 |
19 |
20 | >;
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/(main)/styles.css:
--------------------------------------------------------------------------------
1 | .text-header-home {
2 | color: white;
3 | }
--------------------------------------------------------------------------------
/src/app/api/auth/mcNameToUuid/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 |
3 | export async function GET(req: NextRequest | Request) {
4 | const { searchParams } = new URL(req.url);
5 | const username = searchParams.get('username');
6 |
7 | let uuid: string | undefined = undefined;
8 |
9 | try {
10 | uuid = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`)
11 | .then(res => res?.json())
12 | .then(res => res?.id);
13 | } catch { /* Do nothing */ }
14 |
15 | if (!uuid) { // Fallback in case Mojang's API is down
16 | try {
17 | uuid = await fetch(`https://playerdb.co/api/player/minecraft/${username}`)
18 | .then(res => res?.json())
19 | .then(res => res?.data?.player?.raw_id);
20 | } catch { /* Do nothing */ }
21 | }
22 |
23 | return Response.json({ uuid });
24 | }
--------------------------------------------------------------------------------
/src/app/api/auth/route.ts:
--------------------------------------------------------------------------------
1 | import { defaultSession } from "@/libs/session/iron";
2 | import getSession from "@/libs/session/getSession";
3 |
4 | // read session
5 | export async function GET() {
6 | const session = await getSession();
7 |
8 | if (!session.isLoggedIn) {
9 | return Response.json(defaultSession);
10 | }
11 | return Response.json(session);
12 | }
13 |
14 | // logout
15 | export async function DELETE() {
16 | const session = await getSession();
17 | session.destroy();
18 |
19 | return Response.json(defaultSession);
20 | }
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gaqt/Forums-Frontend/83410e5078611794a8f12f434aefc4524ac28cde/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | transition-timing-function: cubic-bezier(0, 0, .25, 1);
9 | transition-duration: 200ms;
10 | transition-property: all;
11 | text-rendering: optimizeLegibility;
12 | }
13 |
14 | html {
15 | scrollbar-color: oklch(var(--p)) oklch(var(--b3));
16 | scrollbar-width: thin;
17 | scroll-behavior: smooth;
18 | }
19 | ::-webkit-scrollbar {
20 | width: 5px;
21 | }
22 | ::-webkit-scrollbar-track {
23 | background-color: oklch(var(--n));
24 | }
25 | ::-webkit-scrollbar-thumb {
26 | background-color: oklch(var(--p));
27 | }
28 |
29 | /* [data-theme="dark"] .bg-base-400 {
30 |
31 | }
32 | [data-theme="light"] .bg-base-400 {
33 |
34 | } */
35 |
36 | /* Typography (18px base size, 1.333 ratio) */
37 | /* Header fonts */
38 | h1, h2, h3, h4, h5, h6 {
39 | @apply font-header;
40 | }
41 | body {
42 | @apply font-body;
43 | }
44 | strong, .btn {
45 | @apply font-important;
46 | }
47 | h1 {
48 | font-size: 3.22rem;
49 | line-height: 4.25rem;
50 | }
51 | h2 {
52 | font-size: 2.576rem;
53 | line-height: 3.22rem;
54 | }
55 | h3 {
56 | font-size: 2.061rem;
57 | line-height: 2.576rem;
58 | }
59 | h4 {
60 | font-size: 1.648rem;
61 | line-height: 2.061rem;
62 | }
63 | h5 {
64 | font-size: 1.319rem;
65 | line-height: 1.648rem;
66 | }
67 | .text-base {
68 | font-size: 1.055rem;
69 | line-height: 1.319rem;
70 | }
71 | h6 {
72 | @apply text-base;
73 | }
74 | /* Body fonts */
75 | body {
76 | @apply text-base;
77 | }
78 | small {
79 | font-size: 0.844rem;
80 | line-height: 1.055rem;
81 | }
82 | small.smaller {
83 | font-size: 0.675rem;
84 | line-height: 0.844rem;
85 | }
86 | .btn {
87 | font-size: inherit;
88 | line-height: inherit;
89 | }
90 | a {
91 | @apply hover:text-primary;
92 | text-decoration: none;
93 | }
94 |
95 | input {
96 | background-color: var(--fallback-b1,oklch(var(--b1)));
97 | padding: 4px 8px;
98 | font-size: 0.844rem;
99 | line-height: 1.055rem;
100 | }
101 | input::placeholder {
102 | color: var(--fallback-b1,oklch(var(--bc)));
103 | }
104 |
105 | .bg-full {
106 | background-size: cover;
107 | background-repeat: no-repeat;
108 | background-attachment: scroll;
109 | background-position: center;
110 | }
111 |
112 | hr {
113 | @apply bg-base-content;
114 | display: inline-block;
115 | width: 98%;
116 | height: 1px;
117 | margin: 1px 0;
118 | border: 0;
119 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import HTTPClient from '@/libs/HTTPClient';
2 | import './globals.css';
3 | import type { Metadata, /* ResolvingMetadata */ } from 'next';
4 | import { Dosis } from 'next/font/google'
5 | import React from "react";
6 | import { isResultError } from '@/libs/Utils';
7 |
8 | const dosis = Dosis({subsets: ["latin"], variable: "--font-dosis"});
9 |
10 | export default async function RootLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | const res = await (new HTTPClient(process.env.API_URL!)
16 | .GetAsync("/istheapiworking"));
17 |
18 | if (res == null || isResultError(res)) {
19 | console.error("Error fetching API");
20 | return
21 | MCCade
22 | Sorry, it seems we are running through some technical issues, please try again later
23 |
24 | }
25 |
26 |
27 | return (
28 |
29 |
30 | {children}
31 |
32 |
33 | )
34 | }
35 |
36 | //#region Metadata
37 | const url = new URL(process.env.NEXT_PUBLIC_CURR_DOMAIN || '');
38 | const title = 'MCCade Games';
39 | const description = 'A minigame server with a great variety of gamemodes. Made with ❣ and dedication, from us to you. Enjoy!';
40 | const images: string[] = [
41 | url.toString() + 'img/banner.png'
42 | ];
43 | export const metadata: Metadata = {
44 | metadataBase: url,
45 | title: title,
46 | description: description,
47 | openGraph: {
48 | siteName: title,
49 | type: "website",
50 | emails: ['emily@pinkcloud.studio', 'elaina@pinkcloud.studio'],
51 | locale: 'en_GB',
52 | url: url,
53 | title: title,
54 | description: description,
55 | images: images
56 | },
57 | twitter: {
58 | card: 'summary_large_image',
59 | creator: '@NekoElynn',
60 | title: title,
61 | description: description,
62 | images: images
63 | }
64 | }
65 | // type Props = {
66 | // params: { id: string }
67 | // searchParams: { [key: string]: string | string[] | undefined }
68 | // }
69 | // export async function generateMetadata(
70 | // { params, searchParams }: Props,
71 | // parent: ResolvingMetadata,
72 | // ): Promise {
73 | // return {
74 | // ...metadata, // Write dynamic overrides here
75 | // }
76 | // }
77 | //#endregion
78 |
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next'
2 |
3 | export default function Robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: '*',
7 | allow: '/',
8 | },
9 | sitemap: 'https://mccade.net/sitemap.xml',
10 | }
11 | }
--------------------------------------------------------------------------------
/src/app/sitemap.tsx:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next'
2 |
3 | export default function Sitemap(): MetadataRoute.Sitemap {
4 | return [
5 | {
6 | url: 'https://mccade.net',
7 | lastModified: new Date(),
8 | changeFrequency: 'monthly',
9 | priority: 1,
10 | },
11 | ]
12 | }
--------------------------------------------------------------------------------
/src/components/ClipboardTooltip/styles.css:
--------------------------------------------------------------------------------
1 |
2 | .tooltip-link:hover {
3 | cursor:pointer;
4 | }
5 |
6 | .tooltip-spike {
7 | width: 0;
8 | height: 0;
9 | border-left: 10px solid transparent;
10 | border-right: 10px solid transparent;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/DiscordWidget/component.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import './styles.css';
4 | import { useEffect, useState } from 'react';
5 | import useTheme from '@/hooks/useTheme';
6 | import Image from 'next/image';
7 |
8 | interface DiscordWidgetProps {
9 | guildId: string,
10 | className?: string,
11 | }
12 | const DiscordWidget = (props: DiscordWidgetProps) => {
13 | const { guildId, className } = props;
14 | const [theme] = useTheme();
15 | const [userN, setUserN] = useState(-1);
16 |
17 | useEffect(() => {
18 | fetch(`https://discord.com/api/guilds/${guildId}/widget.json`)
19 | .then(r => r?.json()).then((r: any) => setUserN(r["members"].length));
20 | }, [guildId]);
21 |
22 | return (
23 |
24 |
25 |
26 |
{userN} Online
27 |
28 |
29 |
34 |
35 |
40 |
41 | );
42 | }
43 |
44 | export default DiscordWidget;
--------------------------------------------------------------------------------
/src/components/DiscordWidget/styles.css:
--------------------------------------------------------------------------------
1 | .widget {
2 | --border-color: rgba(50, 50, 50, 1);
3 | border:solid 1.5px var(--border-color)
4 | }
5 | [data-theme="light"] .widget {
6 | --border-color: rgba(185, 185, 185, 1);
7 | }
8 |
9 | .discord-widget iframe {
10 | margin-top: -74px;
11 | margin-bottom: -43px;
12 | }
13 | .discord-widget .widget-override {
14 | border-color: var(--border-color);
15 | background: #5865F2;
16 | }
17 | [data-theme="dark"] .discord-widget .widget-override {
18 | background:oklch(var(--b3))
19 | }
--------------------------------------------------------------------------------
/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | interface DropdownProps {
2 | indicatorValue?: any,
3 | indicatorIcon: any,
4 | indicatorClass?: string,
5 | dropdownClassName?: string,
6 | className?: string,
7 | children: any
8 | }
9 | const Dropdown = ({
10 | indicatorValue: indicatorValue,
11 | indicatorIcon: icon,
12 | indicatorClass: indicatorClass,
13 | dropdownClassName: dropdownClassName,
14 | className: className,
15 | children: children
16 | }: DropdownProps) => {
17 | indicatorClass ??= '';
18 |
19 | return (
20 |
21 |
30 |
31 | { children }
32 |
33 |
34 | );
35 | }
36 |
37 | export default Dropdown;
--------------------------------------------------------------------------------
/src/components/HashLink.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Url } from "next/dist/shared/lib/router/router";
3 | import Link from "next/link";
4 | import {CSSProperties} from "react";
5 |
6 | /**
7 | * @deprecated Use Link instead
8 | */
9 | const HashLink = (props: { href: Url; children: any; className?: string; style?: CSSProperties }) => (
10 | {
12 | // Checks if href isn't empty;
13 | const href = props.href.toString();
14 | if (!href.length) return;
15 |
16 | // Checks if origin is current domain;
17 | const origin = window.location.origin;
18 | const url = href.charAt(0) === '/' ? new URL(href, origin) : new URL(href);
19 | if (url.origin !== origin) return;
20 |
21 | // Then, updates.
22 | window.location.hash = url.hash;
23 | }}
24 | >
25 | {props.children}
26 |
27 | );
28 |
29 | export default HashLink;
30 |
--------------------------------------------------------------------------------
/src/components/HeaderContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import useGlobal from '@/hooks/useGlobal';
3 | import { useEffect } from 'react';
4 |
5 | interface HeaderContextProps {
6 | setTo: [string, string];
7 | }
8 | const HeaderContext = ({ setTo: setTo }: HeaderContextProps) => {
9 | const [_, setHeaderContent] = useGlobal<[string, string]>('headerContent');
10 | useEffect(() =>
11 | setHeaderContent(setTo)
12 | , [setHeaderContent, setTo]);
13 | return <>>;
14 | }
15 | export default HeaderContext;
--------------------------------------------------------------------------------
/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { Url } from "next/dist/shared/lib/router/router";
2 | import HashLink from "./HashLink";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 |
6 | const Logo = (props: { href: Url; className?: string; }) => (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default Logo;
15 |
--------------------------------------------------------------------------------
/src/components/Minecraft/Client.tsx:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 | import { MCBust, MCHead, MCProps } from './base';
3 | import useMcUuid from '@/hooks/useMcUuid';
4 |
5 | export const ClientMCBust = (props: MCProps) => {
6 | const { username, className, shadowColor } = props;
7 | const uuid = useMcUuid(username);
8 | return ;
9 | }
10 |
11 | export const ClientMCHead = (props: MCProps) => {
12 | const { username, className, shadowColor } = props;
13 | const uuid = useMcUuid(username);
14 | return ;
15 | }
--------------------------------------------------------------------------------
/src/components/Minecraft/MCServerWidget/component.tsx:
--------------------------------------------------------------------------------
1 | import { getPlayerCount } from '@/services/base/ServerStatsService';
2 | import './styles.css';
3 | import React from 'react';
4 |
5 | interface MCServerWidgetProps {
6 | className?: string,
7 | }
8 | const MCServerWidget = async (props: MCServerWidgetProps) => {
9 | const { className } = props;
10 |
11 | const playerCount = await getPlayerCount();
12 |
13 | return (
14 |
15 |
16 |
17 |
mccade.net
18 | {playerCount
19 | ? {playerCount} Online
20 | : Offline}
21 |
22 |
23 | );
24 | }
25 |
26 | export default MCServerWidget;
27 |
--------------------------------------------------------------------------------
/src/components/Minecraft/MCServerWidget/styles.css:
--------------------------------------------------------------------------------
1 | .widget-filter {
2 | --filter-brightness: brightness(100%);
3 | cursor: pointer;
4 | }
5 | .widget-filter::before {
6 | content: '';
7 | position: absolute;
8 | inset: 0;
9 | width: 100%;
10 | height: 100%;
11 | backdrop-filter: blur(2px) var(--filter-brightness);
12 | }
13 | .widget-filter > div {
14 | width: 100%;
15 | height: 100%;
16 | backdrop-filter: brightness(30%) blur(4px);
17 | border-radius: 10px;
18 | }
19 | .widget-filter:hover {
20 | --filter-brightness: brightness(130%);
21 | }
--------------------------------------------------------------------------------
/src/components/Minecraft/Server.tsx:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 | import { MCBust, MCHead, MCProps } from './base';
3 | import { getUuid } from '@/services/forum/account/AccountService';
4 |
5 | export const ServerMCBust = async (props: MCProps) =>
6 | await getFromFunc(MCBust, props);
7 |
8 | export const ServerMCHead = async (props: MCProps) =>
9 | await getFromFunc(MCHead, props);
10 |
11 | const getFromFunc = async (Func: (props: MCProps) => React.JSX.Element, props: MCProps) => {
12 | let { uuid, username, className, shadowColor } = props;
13 | if (!uuid && !username) return null;
14 | if (!uuid) uuid = await getUuid(username!);
15 | return ;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Minecraft/styles.css:
--------------------------------------------------------------------------------
1 | .mc-bust {
2 | top: calc(50% - 60px);
3 | left: calc(50% - 49px);
4 |
5 | --shadow-color: #000000;
6 | filter: drop-shadow(0px 0px 3px var(--shadow-color));
7 | }
8 |
9 | [data-theme="light"] .mc-bust {
10 | --shadow-color: #8e8e8e;
11 | }
--------------------------------------------------------------------------------
/src/components/MobileNavbar/component.tsx:
--------------------------------------------------------------------------------
1 | import { FaBars } from 'react-icons/fa6';
2 | import './styles.css'
3 |
4 | const MobileNavbar = (props: { className?: string; content: any; includeInput?: boolean; }) => {
5 | const includeInput = props.includeInput ?? true;
6 | return <>
7 | {includeInput ? : <>>}
8 |
15 | >
16 | }
17 |
18 | export default MobileNavbar;
19 |
20 | export const MobileNavToggle = (props: {className?: string}) =>
21 | ;
--------------------------------------------------------------------------------
/src/components/MobileNavbar/styles.css:
--------------------------------------------------------------------------------
1 | .drawer-toggle[type="checkbox"]:checked ~ .drawer-side > .drawer-overlay {
2 | @apply backdrop-filter backdrop-brightness-[.75];
3 | background-color: transparent;
4 | }
5 | @media (max-width: 865px) {
6 | .drawer-side {
7 | display: grid;
8 | }
9 | }
10 | .drawer-side .navlink svg {
11 | width: 20px;
12 | height: 20px;
13 | }
14 | .sidebar-filter {
15 | opacity: 90%;
16 | }
17 | [data-theme="light"] .sidebar-filter {
18 | opacity: 80%;
19 | }
--------------------------------------------------------------------------------
/src/components/NavLink/component.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import './styles.css'
3 | import { usePathname } from 'next/navigation';
4 | import {CSSProperties} from "react";
5 | import Link from 'next/link';
6 |
7 | const NavLink = (props: { href: string; className?: string; children: any; style?: CSSProperties }) => {
8 | const { href, children } = props;
9 | const currRoute = usePathname();
10 |
11 | const isActive = href!="/"
12 | ? currRoute.startsWith(href)
13 | : currRoute == href;
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 | export default NavLink;
22 |
--------------------------------------------------------------------------------
/src/components/NavLink/styles.css:
--------------------------------------------------------------------------------
1 | .navlink {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | }
6 | .navlink h6 {
7 | position: relative;
8 | }
9 | .navlink svg {
10 | width: 18px;
11 | height: 18px;
12 | margin-right: 10px;
13 | }
14 |
15 | a.navlink.btn:hover {
16 | @apply hover:text-base-content;
17 | }
18 | .navlink.active h6::after {
19 | content: '';
20 | position: absolute;
21 | bottom: -3px;
22 | left: 0%;
23 | width: 15px;
24 | height: 2px;
25 | background-color: currentColor;
26 | }
27 | .navlink.active.btn {
28 | border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));
29 | background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
30 | }
31 | @supports (color: color-mix(in oklab, black, black)) {
32 | .navlink.active.btn {
33 | background-color: color-mix(in oklab, oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, black );
34 | border-color: color-mix(in oklab, oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, black );
35 | }
36 | }
--------------------------------------------------------------------------------
/src/components/NewsWidget/component.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import './styles.css';
4 | import React from 'react';
5 | import Image from 'next/image';
6 | import Link from 'next/link';
7 |
8 | const fallbackImage = "/img/mccade-stamp.png"
9 | interface NewsWidgetProps {
10 | src: string;
11 | title: string;
12 | body: string;
13 | className?: string;
14 | useFallback?: boolean;
15 | href: string;
16 | }
17 | const NewsWidget = (props: NewsWidgetProps) => {
18 | let { src, title, body, className, useFallback, href } = props;
19 | if (useFallback === true)
20 | src = fallbackImage;
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default NewsWidget;
41 |
--------------------------------------------------------------------------------
/src/components/NewsWidget/styles.css:
--------------------------------------------------------------------------------
1 | .news-widget .inner {
2 | row-gap: 1rem;
3 | }
4 | .news-widget .inner > div {
5 | min-width: 285px;
6 | width: 100%;
7 | max-width: 285px;
8 | }
9 | .news-widget .img-container {
10 | @apply rounded-lg;
11 | overflow: hidden;
12 | }
13 |
14 | .news-widget .content .title {
15 | color: #252525
16 | }
17 | [data-theme="dark"] .news-widget .content .title {
18 | @apply text-gray-200;
19 | }
20 | .news-widget .img-container img {
21 | filter: drop-shadow(0px 4px 0px #00000045) drop-shadow(0px 0px 20px #00000073);
22 | }
23 | .news-widget .img-container img:hover {
24 | transform: scale(1.12)
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ShrinkableSearch/component.tsx:
--------------------------------------------------------------------------------
1 | import './styles.css'
2 | import { BsSearchHeart } from "react-icons/bs";
3 |
4 | const ShrinkableSearch = () => (
5 |
12 | );
13 |
14 | export default ShrinkableSearch;
--------------------------------------------------------------------------------
/src/components/ShrinkableSearch/styles.css:
--------------------------------------------------------------------------------
1 | .shrinkable-search {
2 | min-width: 135px;
3 | }
4 | .shrinkable-search .form-control {
5 | overflow-y: auto;
6 | overscroll-behavior: contain;
7 | scrollbar-width: none;
8 |
9 | transition: flex 0.5s ease-in-out;
10 | flex-basis: 0;
11 |
12 | min-width: 0;
13 | }
14 | .shrinkable-search #searchToggle:checked ~ div.form-control {
15 | flex-basis: 280px;
16 | }
--------------------------------------------------------------------------------
/src/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/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/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/components/Table/styles.css:
--------------------------------------------------------------------------------
1 | .table-grid {
2 | display: grid;
3 | align-items: stretch;
4 | justify-content: stretch;
5 | align-content: space-between;
6 | }
--------------------------------------------------------------------------------
/src/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { MouseEventHandler } from "react";
3 | import useTheme from '@/hooks/useTheme';
4 |
5 | const ThemeToggle = (props: { className?: string }) => {
6 | const [theme, setTheme] = useTheme('dark');
7 | const isDark = theme !== 'light';
8 |
9 | return (
10 |
11 |
12 |
setTheme('dark')}/>
13 | setTheme('light') : undefined}/>
14 |
15 | );
16 | }
17 |
18 | const LightIcon = (props: { onClick?: MouseEventHandler; className?: string; }) => (
19 |
34 | );
35 |
36 | const DarkIcon = (props: { onClick?: MouseEventHandler; className?: string; }) => (
37 |
52 | );
53 |
54 | export default ThemeToggle;
--------------------------------------------------------------------------------
/src/hooks/useGlobal.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useCallback, useEffect, useState } from "react";
3 | import { BehaviorSubject } from "rxjs";
4 |
5 | declare namespace globalThis {
6 | let elyGlobals: {[key: string]: BehaviorSubject};
7 | }
8 | globalThis.elyGlobals ??= {};
9 |
10 | function useGlobal(
11 | variableId: string,
12 | defaultValueGetter?: () => T,
13 | setupCall?: ($: BehaviorSubject) => void
14 | ):
15 | [T | undefined, (variable: T) => void]
16 | {
17 | const elyGlobals = globalThis.elyGlobals;
18 | const subjectName = `${variableId}$`
19 | const [variable, setVariableState] = useState();
20 | const this$ = elyGlobals[subjectName];
21 | const setVariable = useCallback((variable: T) => this$?.next(variable), [this$]);
22 |
23 | useEffect(() => {
24 | if (elyGlobals[subjectName] === undefined) {
25 | const $ = new BehaviorSubject(defaultValueGetter?.() ?? undefined as T);
26 | elyGlobals[subjectName] = $;
27 | setupCall?.($);
28 | }
29 |
30 | const subscription = elyGlobals[subjectName].subscribe((variable) => setVariableState(variable));
31 | return () => {
32 | subscription.unsubscribe();
33 | if (!elyGlobals[subjectName].observed) {
34 | elyGlobals[subjectName].complete();
35 | delete elyGlobals[subjectName];
36 | }
37 | }
38 | }, [elyGlobals, subjectName, defaultValueGetter, setupCall]);
39 |
40 | return [variable, setVariable];
41 | }
42 |
43 | export default useGlobal;
--------------------------------------------------------------------------------
/src/hooks/useHash.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useState } from 'react';
3 |
4 | const getHash = () => (typeof window !== 'undefined' ? decodeURIComponent(window.location.hash) : '');
5 |
6 | const useHash = () => {
7 | const [hash, setHash] = useState(getHash());
8 |
9 | useEffect(() => {
10 | const onHashChanged = () => {
11 | setHash(getHash());
12 | };
13 |
14 | window.addEventListener("hashchange", onHashChanged);
15 | return () => window.removeEventListener("hashchange", onHashChanged);
16 | }, []);
17 |
18 | return hash;
19 | };
20 |
21 | export default useHash;
--------------------------------------------------------------------------------
/src/hooks/useMcUuid.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const useMcUuid = (username?: string): string | undefined => {
4 | const [uuid, setUuid] = useState();
5 |
6 | useEffect(() => {
7 | if (!username) return;
8 |
9 | try {
10 | fetch(`/api/auth/mcNameToUuid?username=${username}`)
11 | .then(res => res?.json())
12 | .then(res => setUuid(res?.uuid));
13 | } catch { /* Do nothing */ }
14 | }, [username]);
15 |
16 | return uuid;
17 | }
18 | export default useMcUuid;
--------------------------------------------------------------------------------
/src/hooks/useSession.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SessionData, defaultSession } from "@/libs/session/iron";
3 | import { Login, Register, Reset } from "@/services/forum/account/AccountService";
4 | import { BehaviorSubject } from 'rxjs';
5 | import useGlobal from "./useGlobal";
6 | import { useCallback } from "react";
7 |
8 | interface SessionManager {
9 | session?: SessionData;
10 | register: (body: any) => Promise<[SessionData | null, number, string | null]>;
11 | reset: (body: any) => Promise<[SessionData | null, number, string | null]>;
12 | login: (body: any) => Promise<[SessionData | null, number, string | null]>;
13 | logout: () => Promise;
14 | }
15 | function useSession(): SessionManager {
16 | const runOnceGlobally = useCallback(
17 | ($: BehaviorSubject) =>
18 | fetch("/api/auth")
19 | .then((res) => res.json())
20 | .then((session) => $.next(session)), []
21 | );
22 |
23 | const getDefaultSession = useCallback(() => defaultSession, []);
24 | const [session, setSession] = useGlobal('session', getDefaultSession, runOnceGlobally);
25 |
26 | const InterceptSession =
27 | (method: any): (body: any) => Promise<[SessionData | null, number, string | null]> =>
28 | useCallback(async (body: any) => {
29 | const result = await method(body);
30 | if (result[0])
31 | setSession(result[0]);
32 | return result;
33 | }, [method]);
34 |
35 | const register = InterceptSession(Register);
36 | const reset = InterceptSession(Reset);
37 | const login = InterceptSession(Login);
38 |
39 | const logout = useCallback(async () => {
40 | await fetch("/api/auth", { method: "delete" })
41 | .then((res) => res.json())
42 | .then((session) => {
43 | setSession(session);
44 | })
45 | .then(() => window.location.reload());
46 | }, [setSession]);
47 |
48 | return {
49 | session,
50 | register,
51 | reset,
52 | login,
53 | logout
54 | };
55 | }
56 | export default useSession;
57 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useCallback, useEffect } from "react";
3 | import useGlobal from './useGlobal';
4 |
5 | function useTheme(defaultTheme: string = "dark"): [string | undefined, (theme: string) => void] {
6 | const getDefaultTheme = useCallback(() => window.localStorage.getItem("theme") ?? defaultTheme, [defaultTheme]);
7 | const [theme, setTheme] = useGlobal('theme', getDefaultTheme);
8 |
9 | useEffect(() => {
10 | if (!theme) return;
11 |
12 | const root = window.document.documentElement;
13 | root.setAttribute("data-theme", theme);
14 | window.localStorage.setItem("theme", theme);
15 | }, [theme]);
16 |
17 | return [theme, setTheme];
18 | }
19 |
20 | export default useTheme;
--------------------------------------------------------------------------------
/src/libs/Utils.ts:
--------------------------------------------------------------------------------
1 | export const stringToDate = (dateString?: string) => {
2 | if (!dateString) return null;
3 | const dateInt = tryParseInt(dateString);
4 | return !dateInt || dateInt === -1 ? null : new Date(dateInt);
5 | };
6 |
7 | export const tryParseInt = (intAsStr?: string) => {
8 | if (!intAsStr) return null;
9 | try {return parseInt(intAsStr)} catch { }
10 | return null;
11 | };
12 |
13 | export const toLocaleString = (date?: Date | null) => {
14 | if (!date) return "Never";
15 | return date.toLocaleString();
16 | };
17 |
18 | export const isResultError = (res: any, isNullable: boolean = true) =>
19 | !(res[1] >= 200 && res[1] <= 299) || (!isNullable && !res[0]);
20 |
21 | export const awaitAll = async (promises?: Promise[]): Promise => {
22 | if (!promises) return [];
23 | const results: any[] = [];
24 | if (promises) {
25 | for (const promise of promises)
26 | results.push(await promise);
27 | }
28 | return results;
29 | }
30 |
31 | // https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
32 | export const newUuid = () => {
33 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c: any) =>
34 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/libs/session/getSession.ts:
--------------------------------------------------------------------------------
1 | import { getIronSession } from "iron-session";
2 | import { cookies } from "next/headers";
3 | import { SessionData, defaultSession, sessionOptions } from "./iron"
4 |
5 | export default async function getSession() {
6 | const session = await getIronSession(cookies(), sessionOptions);
7 |
8 | if (!session.isLoggedIn) {
9 | session.username = defaultSession.username;
10 | session.uuid = defaultSession.uuid;
11 | session.email = defaultSession.email;
12 | session.isLoggedIn = false;
13 | }
14 |
15 | return session;
16 | }
--------------------------------------------------------------------------------
/src/libs/session/iron.ts:
--------------------------------------------------------------------------------
1 | import { SessionOptions } from "iron-session";
2 |
3 | export interface SessionData {
4 | username: string;
5 | uuid: string;
6 | email: string;
7 | isLoggedIn: boolean;
8 | }
9 | export const defaultSession: SessionData = {
10 | username: "",
11 | uuid: "",
12 | email: "",
13 | isLoggedIn: false,
14 | };
15 |
16 | export const sessionOptions: SessionOptions = {
17 | password: process.env.SESSION_SECRET!,
18 | cookieName: "session",
19 | cookieOptions: {
20 | // secure only works in `https` environments
21 | // if your localhost is not on `https`, then use: `secure: process.env.NODE_ENV === "production"`
22 | secure: true,
23 | },
24 | };
--------------------------------------------------------------------------------
/src/libs/types/entities/Account.ts:
--------------------------------------------------------------------------------
1 | export default interface Account {
2 | _id: string,
3 | email: string,
4 | password?: string,
5 | token: string,
6 | settings: {[key: string]: string}
7 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/Forum.ts:
--------------------------------------------------------------------------------
1 | import Thread from "./Thread";
2 |
3 | export default interface Forum {
4 | _id: string,
5 | name: string,
6 | description: string,
7 | weight: number,
8 | locked: boolean,
9 | category: string,
10 | categoryName: string,
11 | categoryWeight: number,
12 | threadAmount: number
13 | lastThread?: Thread;
14 | }
15 |
--------------------------------------------------------------------------------
/src/libs/types/entities/ForumCategory.ts:
--------------------------------------------------------------------------------
1 | import Forum from "./Forum";
2 |
3 | export default interface ForumCategory {
4 | _id: string;
5 | name: string;
6 | weight: number;
7 | forums: Forum[];
8 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/Grant.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface Scope {
3 | scope: string,
4 | }
5 |
6 | export default interface Grant {
7 | rankId: string,
8 | scopes: Scope[],
9 | _id: string,
10 | target: string,
11 | active: string,
12 | issuedAt: string,
13 | issuedBy: string,
14 | issuedOn: string
15 | reason: string,
16 | removedAt: string,
17 | removedBy: string,
18 | removedOn: string,
19 | removedReason: string,
20 | duration: string,
21 | permanent: string,
22 | }
23 |
--------------------------------------------------------------------------------
/src/libs/types/entities/Permission.ts:
--------------------------------------------------------------------------------
1 | export default interface Permission {
2 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/Profile.ts:
--------------------------------------------------------------------------------
1 | export interface PermissionScope {
2 | scope: string;
3 | }
4 |
5 | export interface ProfilePermission {
6 | permission: string;
7 | expiration: number;
8 | scopes: PermissionScope[];
9 | }
10 |
11 | export interface Cooldown {
12 | start: number;
13 | expire: number;
14 | name: string;
15 | }
16 |
17 | export interface Note {
18 | uuid: string;
19 | issuedBy: string;
20 | issuedAt: number;
21 | note: string;
22 | }
23 |
24 | export interface Skin {
25 | name: string;
26 | value: string;
27 | signature: string;
28 | }
29 |
30 | export interface DisguiseData {
31 | realName: string;
32 | disguiseName: string;
33 | rankId: string;
34 | realSkin: string;
35 | disguiseSkin: string;
36 | }
37 |
38 | export interface Login {
39 | time: number;
40 | ip: string;
41 | }
42 |
43 | export default interface Profile {
44 | name: string;
45 | _id: string;
46 | nameToLowercase: string;
47 | permissions: ProfilePermission[];
48 | attachmentPermissions: string[];
49 | ignoring: string[];
50 | cooldowns: Cooldown[];
51 | alts: string[];
52 | notes: Note[];
53 | currentAddress: string;
54 | knownAddresses: string[];
55 | disguiseData: DisguiseData;
56 | tagName: string;
57 | authType: string;
58 | twoFactor: boolean;
59 | logins: Login[];
60 | metadata: {[key:string]: string}
61 | }
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/libs/types/entities/ProfileConnections.ts:
--------------------------------------------------------------------------------
1 |
2 | export default interface ProfileConnections {
3 | discord: string | undefined,
4 | twitter: string | undefined,
5 | instagram: string | undefined,
6 | youtube: string | undefined,
7 | }
8 |
--------------------------------------------------------------------------------
/src/libs/types/entities/Punishment.ts:
--------------------------------------------------------------------------------
1 | export interface Proof {
2 | type: string,
3 | proof: string,
4 | addedBy: string,
5 | }
6 |
7 | export default interface Punishment {
8 | punishmentType: string,
9 | silent: boolean,
10 | removedSilent: boolean,
11 | ip: boolean,
12 | voided: boolean,
13 | shadow: boolean,
14 | punishmentID: string,
15 | address: string,
16 | proof: Proof[],
17 | active: boolean,
18 | duration: number,
19 | issuedAt: string,
20 | issuedBy: string,
21 | issuedOn: string,
22 | permanent: boolean,
23 | reason: string,
24 | removedAt: string,
25 | removedBy: string,
26 | removedOn: string,
27 | removedReason: string,
28 | target: string,
29 | }
30 |
--------------------------------------------------------------------------------
/src/libs/types/entities/Rank.ts:
--------------------------------------------------------------------------------
1 | import Permission from './Permission';
2 | import Scope from './Scope';
3 |
4 | export default interface Rank {
5 | _id: string;
6 | name: string;
7 | defaultRank: boolean;
8 | priority: number;
9 | price: number;
10 | permissions: Permission[];
11 | inheritance: string[];
12 | scopes: Scope[];
13 | prefix: string;
14 | displayName: string;
15 | suffix: string;
16 | color: string;
17 | playerListPrefix: string;
18 | visible: boolean;
19 | staff: boolean;
20 | subscription: boolean;
21 | grantable: boolean;
22 | purchasable: boolean
23 | }
24 |
--------------------------------------------------------------------------------
/src/libs/types/entities/Scope.ts:
--------------------------------------------------------------------------------
1 | export default interface Scope {
2 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/SkywarsStats.ts:
--------------------------------------------------------------------------------
1 |
2 | export default interface SkywarsStats {
3 | deaths: string,
4 | elo: string,
5 | heads: string,
6 | highestKillStreak: string,
7 | kills: string,
8 | killStreak: string,
9 | souls: string,
10 | wins: string,
11 | }
12 |
13 | export const SKYWARS_NICENAME = {
14 | deaths: "Deaths",
15 | elo: "ELO",
16 | heads: "Heads",
17 | highestKillStreak: "Highest Kill Streak",
18 | kills: "Kills",
19 | killStreak: "Kill Streak",
20 | souls: "Souls",
21 | wins: "Wins",
22 | }
23 |
--------------------------------------------------------------------------------
/src/libs/types/entities/TextFilter.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export interface TextFilter {
4 | _id: string,
5 | filter: string,
6 | filterType: string,
7 | hard: boolean,
8 | }
9 |
--------------------------------------------------------------------------------
/src/libs/types/entities/Thread.ts:
--------------------------------------------------------------------------------
1 | export default interface Thread {
2 | _id: string,
3 | title: string,
4 | body: string,
5 | forum: string,
6 | author: string,
7 | createdAt: string,
8 | lastEditedBy?: string | null,
9 | lastEditedAt: string,
10 | lastReplyAt: string,
11 | pinned: boolean,
12 | locked: boolean,
13 | parentThreadId: string,
14 | authorName: string,
15 | authorWebColor: string,
16 | forumName: string,
17 | replies: Thread[] // Replies are subthreads
18 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/Ticket.ts:
--------------------------------------------------------------------------------
1 |
2 | export default interface Ticket {
3 | _id: string,
4 | author: string,
5 | createdAt: string,
6 | lastUpdatedAt: string,
7 | category: string,
8 | title: string,
9 | body: string,
10 | parentTicket: string | null,
11 | status: string,
12 | result: string,
13 | replies: string[],
14 | }
15 |
--------------------------------------------------------------------------------
/src/libs/types/entities/TicketCategory.ts:
--------------------------------------------------------------------------------
1 |
2 | export default interface TicketCategory {
3 | _id: string,
4 | name: string,
5 | }
6 |
--------------------------------------------------------------------------------
/src/libs/types/entities/TicketReply.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * @deprecated use Ticket instead
4 | */
5 | export default interface TicketReply {
6 | id: String,
7 | body: String,
8 | author: String,
9 | createdAt: number,
10 | parentTicketId: String,
11 | }
12 |
--------------------------------------------------------------------------------
/src/libs/types/entities/User.ts:
--------------------------------------------------------------------------------
1 | export default interface User {
2 | }
--------------------------------------------------------------------------------
/src/libs/types/entities/WebEntry.ts:
--------------------------------------------------------------------------------
1 |
2 | export default interface WebEntry {
3 | _id: string,
4 | value: string,
5 | }
6 |
--------------------------------------------------------------------------------
/src/libs/types/extensions/lib.dom.iterable.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface FormData {
3 | toJSO(): { [key: string]: any };
4 | toJSON(): string;
5 | }
6 | }
7 |
8 | FormData.prototype.toJSO = function () {
9 | const object: { [key: string]: FormDataEntryValue | FormDataEntryValue[] } = {};
10 | this.forEach((value, key) => {
11 | if (!Reflect.has(object, key)) {
12 | object[key] = value;
13 | return;
14 | }
15 | if (!Array.isArray(object[key])) {
16 | object[key] = [object[key] as FormDataEntryValue];
17 | }
18 | (object[key] as FormDataEntryValue[]).push(value);
19 | });
20 | return object;
21 | };
22 |
23 | FormData.prototype.toJSON = function () {
24 | return JSON.stringify(this.toJSO());
25 | };
26 |
27 | export {};
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from 'next/server';
2 | import getSession from "./libs/session/getSession";
3 |
4 | export const config = {
5 | matcher: ['/auth/:path*', '/u/:path*', '/forums/:path*', '/support/:path*', '/op/:path*', '/a/:path*', '/support/:path*'],
6 | }
7 | export async function middleware(req: NextRequest) {
8 | const session = await getSession();
9 | const route = req.nextUrl.pathname;
10 |
11 | switch (route.split('/')[1]) {
12 | // Auth Page redirects if user already logged in
13 | case "auth":
14 | if (session.isLoggedIn)
15 | return NextResponse.redirect(new URL('/', req.url), 302);
16 | break;
17 |
18 | // Pages that need URL for loading at server time
19 | case "u":
20 | case "forums":
21 | case "support":
22 | req.headers.set('x-url', req.url);
23 | return NextResponse.next({ request: { headers: req.headers } });
24 |
25 | // Admin Pages
26 | case "op":
27 | const userIsAdmin = true; // Check if user is admin - TODO
28 | if (!userIsAdmin)
29 | return NextResponse.json({
30 | message: 'You do not have permission to see this page. If you think this is a mistake, please contact the Support.'
31 | }, { status: 401 });
32 | break;
33 |
34 | // Protected Pages ask you to login first then redirect you back
35 | case "a":
36 | case "support":
37 | if (!session.isLoggedIn)
38 | return NextResponse.redirect(new URL('/auth/login?' + new URLSearchParams({ redirect: req.url }), req.url), 302);
39 | break;
40 |
41 | default:
42 | break;
43 | }
44 | return NextResponse.next();
45 | }
--------------------------------------------------------------------------------
/src/services/base/ServerStatsService.ts:
--------------------------------------------------------------------------------
1 |
2 | export async function getPlayerCount(): Promise {
3 | const res = await fetch("https://api.mcsrvstat.us/3/mccade.net")
4 | const json = await res.json();
5 | return json?.players?.online || undefined;
6 | }
7 |
--------------------------------------------------------------------------------
/src/services/controller/ChatSnapshotService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Asynchronous endpoint to retrieve chat snapshots by player UUID.
5 | // *
6 | // * @param uuid The UUID of the player for whom chat snapshots are requested.
7 | // * @return ResponseEntity containing a CompletableFuture with a list of ChatSnapshots.
8 | // */
9 | //// In the API: @GetMapping("/snapshots/{uuid}")
10 | //export const GetChatSnapshots = async (uuid: string, client?: HTTPClient) => {
11 | // const uri = `/snapshots/${uuid}`;
12 | //
13 | // client ??= new HTTPClient(process.env.API_URL!);
14 | // const response = await (await client.GetAsync(uri)).json();
15 | //
16 | // return response;
17 | // }
18 | //
19 | ///**
20 | // * Endpoint to retrieve a chat snapshot by its nice ID.
21 | // *
22 | // * @param id The nice ID of the chat snapshot.
23 | // * @return ResponseEntity containing the ChatSnapshot or not found status if not available.
24 | // */
25 | //// In the API: @GetMapping("/snapshots/id/{id}")
26 | //export const GetChatSnapshot = async (id: string, client?: HTTPClient) => {
27 | // const uri = `/snapshots/id/${id}`;
28 | //
29 | // client ??= new HTTPClient(process.env.API_URL!);
30 | // const response = await (await client.GetAsync(uri)).json();
31 | //
32 | // return response;
33 | // }
34 | //
--------------------------------------------------------------------------------
/src/services/controller/CommandLogService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Endpoint to retrieve command logs issued by a player within a specific time range.
5 | // *
6 | // * @param uuid The UUID of the player for whom command logs are requested.
7 | // * @param time The timestamp representing the time range for command logs.
8 | // * @return ResponseEntity containing a list of CommandLogs.
9 | // */
10 | //// In the API: @GetMapping("/commandLogs/{uuid}/{time}")
11 | //export const GetCommandLogs = async (uuid: string, time: string, client?: HTTPClient) => {
12 | // const uri = `/commandLogs/${uuid}/${time}`;
13 | //
14 | // client ??= new HTTPClient(process.env.API_URL!);
15 | // const response = await (await client.GetAsync(uri)).json();
16 | //
17 | // return response;
18 | // }
19 | //
20 | ///**
21 | // * Endpoint to retrieve command logs for a specific server within a time range.
22 | // *
23 | // * @param server The name of the server for which command logs are requested.
24 | // * @param time The timestamp representing the time range for command logs.
25 | // * @return ResponseEntity containing a list of CommandLogs.
26 | // */
27 | //// In the API: @GetMapping("/commandLogs/server/{server}/{time}")
28 | //export const GetServerCommandLogs = async (server: string, time: string, client?: HTTPClient) => {
29 | // const uri = `/commandLogs/server/${server}/${time}`;
30 | //
31 | // client ??= new HTTPClient(process.env.API_URL!);
32 | // const response = await (await client.GetAsync(uri)).json();
33 | //
34 | // return response;
35 | // }
36 | //
--------------------------------------------------------------------------------
/src/services/controller/LeaderboardService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * WARNING: DEPRECATED
5 | // * Endpoint to retrieve leaderboard data for the "soup" category.
6 | // *
7 | // * @return ResponseEntity containing a map with leaderboard statistics and player information.
8 | // */
9 | //// In the API: @GetMapping("/leaderboards/soup")
10 | //export const GetSoupLeaderboard = async (client?: HTTPClient) => {
11 | // const uri = `/leaderboards/soup`;
12 | //
13 | // client ??= new HTTPClient(process.env.API_URL!);
14 | // const response = await (await client.GetAsync(uri)).json();
15 | //
16 | // return response;
17 | // }
18 | //
19 | ///**
20 | // * Endpoint to retrieve leaderboard data for the "bedwars" category.
21 | // *
22 | // * @return ResponseEntity containing a map with leaderboard statistics and player information.
23 | // */
24 | //// In the API: @GetMapping("/leaderboards/bedwars")
25 | //export const GetBedwarsLeaderboard = async (client?: HTTPClient) => {
26 | // const uri = `/leaderboards/bedwars`;
27 | //
28 | // client ??= new HTTPClient(process.env.API_URL!);
29 | // const response = await (await client.GetAsync(uri)).json();
30 | //
31 | // return response;
32 | // }
33 | //
34 | ///**
35 | // * Endpoint to retrieve player statistics for a specific leaderboard category and player UUID.
36 | // *
37 | // * @param category The category of the leaderboard.
38 | // * @param uuid The UUID of the player for whom statistics are requested.
39 | // * @return ResponseEntity containing a list of Pair objects representing player statistics.
40 | // */
41 | //// In the API: @GetMapping("/leaderboards/stats/{category}/{uuid}")
42 | //export const GetLeaderboardStats = async (category: string, uuid: string, client?: HTTPClient) => {
43 | // const uri = `/leaderboards/stats/${category}/${uuid}`;
44 | //
45 | // client ??= new HTTPClient(process.env.API_URL!);
46 | // const response = await (await client.GetAsync(uri)).json();
47 | //
48 | // return response;
49 | // }
50 |
--------------------------------------------------------------------------------
/src/services/controller/NetworkService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Endpoint to retrieve information about all servers.
5 | // *
6 | // * @return ResponseEntity containing a set of ServerData objects representing all servers.
7 | // */
8 | //// In the API: @GetMapping("/servers/all")
9 | //const GetAllServers = async (client?: HTTPClient) => {
10 | // const uri = `/servers/all`;
11 | //
12 | // client ??= new HTTPClient(process.env.API_URL!);
13 | // const response = await (await client.GetAsync(uri)).json();
14 | //
15 | // return response;
16 | // }
17 | //
18 | ///**
19 | // * Endpoint to retrieve information about all proxies.
20 | // *
21 | // * @return ResponseEntity containing a set of ServerData objects representing all proxies.
22 | // */
23 | //// In the API: @GetMapping("/proxies/all")
24 | //const GetAllProxies = async (client?: HTTPClient) => {
25 | // const uri = `/proxies/all`;
26 | //
27 | // client ??= new HTTPClient(process.env.API_URL!);
28 | // const response = await (await client.GetAsync(uri)).json();
29 | //
30 | // return response;
31 | // }
32 | //
33 | //
34 | ///**
35 | // * Endpoint to retrieve information about a specific server by its name.
36 | // *
37 | // * @param name The name of the server.
38 | // * @return ResponseEntity containing the ServerData or not found status if the server is not available.
39 | // */
40 | //// In the API: @GetMapping("/server/{name}")
41 | //export const GetServer = async (name: string, client?: HTTPClient) => {
42 | // const uri = `/server/${name}`;
43 | //
44 | // client ??= new HTTPClient(process.env.API_URL!);
45 | // const response = await (await client.GetAsync(uri)).json();
46 | //
47 | // return response;
48 | // }
49 | //
50 | ///**
51 | // * Endpoint to retrieve information about a specific proxy by its name.
52 | // *
53 | // * @param name The name of the proxy.
54 | // * @return ResponseEntity containing the ServerData or not found status if the proxy is not available.
55 | // */
56 | //// In the API: @GetMapping("/proxy/{name}")
57 | //export const GetProxy = async (name: string, client?: HTTPClient) => {
58 | // const uri = `/proxy/${name}`;
59 | //
60 | // client ??= new HTTPClient(process.env.API_URL!);
61 | // const response = await (await client.GetAsync(uri)).json();
62 | //
63 | // return response;
64 | // }
--------------------------------------------------------------------------------
/src/services/controller/ProfileService.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import HTTPClient from "@/libs/HTTPClient";
4 | import Profile from "@/libs/types/entities/Profile";
5 | import ProfileConnections from "@/libs/types/entities/ProfileConnections";
6 |
7 | /**
8 | * Endpoint to retrieve information about all user profiles.
9 | *
10 | * @return ResponseEntity containing a list of Profile objects representing all profiles.
11 | */
12 | // In the API: @GetMapping("/profile/all")
13 | export const GetAllProfiles = async (client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
14 | await client.GetAsync(`/profile/all`);
15 |
16 | /**
17 | * Endpoint to retrieve a user profile based on their UUID.
18 | *
19 | * @param uuid The UUID of the user.
20 | * @param client
21 | * @return ResponseEntity containing the Profile or not found status if the profile is not available.
22 | */
23 | // In the API: @GetMapping("/profile/{uuid}")
24 | export const GetProfileFromUuid = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
25 | await client.GetAsync(`/profile/${uuid}`);
26 |
27 | /**
28 | * Endpoint to retrieve the server data for a user based on their UUID.
29 | *
30 | * @param uuid The UUID of the user.
31 | * @param client
32 | * @return ResponseEntity containing the ServerData or not found status if the server data is not available.
33 | */
34 | // In the API: @GetMapping("/profile/server/{uuid}")
35 | export const GetServerData = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
36 | await client.GetAsync(`/profile/server/${uuid}`);
37 |
38 | /**
39 | * Endpoint to retrieve the proxy information for a user based on their UUID.
40 | *
41 | * @param uuid The UUID of the user.
42 | * @param client
43 | * @return ResponseEntity containing the proxy information or not found status if the information is not available.
44 | */
45 | // In the API: @GetMapping("/profile/proxy/{uuid}")
46 | export const GetProxyData = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
47 | await client.GetAsync(`/profile/proxy/${uuid}`);
48 |
49 | export const GetPublicConnections = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
50 | await client.GetAsync(`/profile/connections/${uuid}`);
51 |
52 | export const updateConnections = async (uuid: string, body: any, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
53 | await client.PutAsync(`/profile/connections/${uuid}`, body)
54 |
--------------------------------------------------------------------------------
/src/services/controller/RankListService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Endpoint to retrieve the staff rank list.
5 | // *
6 | // * @return ResponseEntity containing a Map with staff ranks categorized by key and a list of corresponding values.
7 | // */
8 | //// In the API: @GetMapping("/staff")
9 | //export const GetStaffRanks = async (client?: HTTPClient) => {
10 | // const uri = `/staff`;
11 | //
12 | // client ??= new HTTPClient(process.env.API_URL!);
13 | // const response = await (await client.GetAsync(uri)).json();
14 | //
15 | // return response;
16 | // }
17 | //
18 | ///**
19 | // * Endpoint to retrieve the media rank list.
20 | // *
21 | // * @return ResponseEntity containing a Map with media ranks categorized by key and a list of corresponding values.
22 | // */
23 | //// In the API: @GetMapping("/media")
24 | //export const GetMediaRanks = async (client?: HTTPClient) => {
25 | // const uri = `/media`;
26 | //
27 | // client ??= new HTTPClient(process.env.API_URL!);
28 | // const response = await (await client.GetAsync(uri)).json();
29 | //
30 | // return response;
31 | // }
--------------------------------------------------------------------------------
/src/services/controller/RankService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Endpoint to retrieve information about all ranks.
5 | // *
6 | // * @return ResponseEntity containing a list of Rank objects representing all ranks.
7 | // */
8 | //// In the API: @GetMapping("/rank/all")
9 | //export const GetAllRanks = async (client?: HTTPClient) => {
10 | // const uri = `/rank/all`;
11 | //
12 | // client ??= new HTTPClient(process.env.API_URL!);
13 | // const response = await (await client.GetAsync(uri)).json();
14 | //
15 | // return response;
16 | // }
17 | //
18 | ///**
19 | // * Endpoint to retrieve the rank information for a user based on their UUID.
20 | // *
21 | // * @param uuid The UUID of the user.
22 | // * @return ResponseEntity containing the Rank or not found status if the rank information is not available.
23 | // */
24 | //// In the API: @GetMapping("/rank/{uuid}")
25 | //export const GetRank = async (uuid: string, client?: HTTPClient) => {
26 | // const uri = `/rank/${uuid}`;
27 | //
28 | // client ??= new HTTPClient(process.env.API_URL!);
29 | // const response = await (await client.GetAsync(uri)).json();
30 | //
31 | // return response;
32 | // }
33 | //
34 | ///**
35 | // * Endpoint to retrieve the rank information for a user based on their rank name.
36 | // *
37 | // * @param name The name of the rank.
38 | // * @return ResponseEntity containing the Rank or not found status if the rank information is not available.
39 | // */
40 | //// In the API: @GetMapping("/rank/name/{name}")
41 | //export const GetRankFromName = async (name: string, client?: HTTPClient) => {
42 | // const uri = `/rank/name/${name}`;
43 | //
44 | // client ??= new HTTPClient(process.env.API_URL!);
45 | // const response = await (await client.GetAsync(uri)).json();
46 | //
47 | // return response;
48 | // }
--------------------------------------------------------------------------------
/src/services/forum/category/CategoryService.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import ForumCategory from '@/libs/types/entities/ForumCategory';
4 | import HTTPClient from "@/libs/HTTPClient";
5 |
6 | /**
7 | * Retrieve all forum categories.
8 | *
9 | * @return ResponseEntity containing a JsonArray of forum categories in the body.
10 | */
11 | // In the API: @GetMapping(path = "/forum/category")
12 | export const GetForumCategories = async (client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
13 | await client.GetAsync(`/forum/category`);
14 |
15 | /**
16 | * Create a new forum category.
17 | *
18 | * @param body (type: { id: string, name: string, weight: number }) The JSON object containing information for creating the new category.
19 | * @return ResponseEntity containing the created forum category in the body.
20 | */
21 | // In the API: @PostMapping(path = "/forum/category")
22 | export const CreateForumCategory = async (id: string, name: string, weight: number, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
23 | await client.PostAsync(`/forum/category`, { id, name, weight });
24 |
25 | /**
26 | * Update an existing forum category.
27 | *
28 | * @param body (type: { name?: string, weight?: int }) The JSON object containing information for updating the category.
29 | * @param id The ID of the category to be updated.
30 | * @return ResponseEntity containing the updated forum category in the body.
31 | */
32 | // In the API: @PutMapping(path = "/forum/category/{id}")
33 | export const UpdateForumCategory = async (id: string, name?: string, weight?: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
34 | await client.PostAsync(`/forum/category/${id}`, {name, weight});
35 |
36 | /**
37 | * Delete an existing forum category.
38 | *
39 | * @param id The ID of the category to be deleted.
40 | * @return ResponseEntity containing the deleted forum category in the body.
41 | */
42 | // In the API: @DeleteMapping(path = "/forum/category/{id}")
43 | export const DeleteForumCategory = async (id: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
44 | await client.DeleteAsync(`/forum/category/${id}`);
--------------------------------------------------------------------------------
/src/services/forum/filter/TextFilterService.ts:
--------------------------------------------------------------------------------
1 | import HTTPClient from "@/libs/HTTPClient";
2 | import { TextFilter } from "@/libs/types/entities/TextFilter";
3 |
4 |
5 | export const getAllFilters = async (client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
6 | await client.GetAsync(`/forum/filter`);
7 |
--------------------------------------------------------------------------------
/src/services/forum/forum/ForumService.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import HTTPClient from "@/libs/HTTPClient";
4 | import Forum from "@/libs/types/entities/Forum";
5 |
6 | /**
7 | * Get forum details, optionally with threads based on page number.
8 | *
9 | * @param id The ID of the forum.
10 | * @param page The page number for retrieving threads (default: -1).
11 | * @return ResponseEntity with forum details or an error message.
12 | */
13 | // In the API: @GetMapping(path = "/forum/forum/{id}")]
14 | export const GetForum = async (id: string, page: number = -1, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
15 | await client.GetAsync(`/forum/forum/${id}`);
16 |
17 | /**
18 | * Create a new forum.
19 | *
20 | * @param body (type: { id: string, name: string, description: string, weight: number, locked: boolean, categoryId: string }) The JSON object containing forum details.
21 | * @return ResponseEntity with the created forum details or an error message.
22 | */
23 | // In the API: @PostMapping(path = "/forum/forum")
24 | export const CreateForum = async (forumData: Forum, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
25 | await client.PostAsync(`/forum/forum`, forumData);
26 |
27 | /**
28 | * Edit an existing forum.
29 | *
30 | * @param body (type: { name?: string, description?: string, weight?: number, locked?: boolean }) The JSON object containing updated forum details.
31 | * @param id The ID of the forum to be edited.
32 | * @return ResponseEntity with the edited forum details or an error message.
33 | */
34 | // In the API: @PutMapping(path = "/forum/forum/{id}")
35 | export const EditForum = async (forum: Forum, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
36 | await client.PutAsync(`/forum/forum/${forum._id}`, forum);
37 |
38 | /**
39 | * Delete a forum.
40 | *
41 | * @param id The ID of the forum to be deleted.
42 | * @return ResponseEntity with the deleted forum details or an error message.
43 | */
44 | // In the API: @DeleteMapping(path = "/forum/forum/{id}")
45 | export const DeleteForum = async (id: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
46 | await client.DeleteAsync(`/forum/forum/${id}`);
47 |
--------------------------------------------------------------------------------
/src/services/forum/punishment/PunishmentService.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import HTTPClient from "@/libs/HTTPClient";
4 | import Punishment from "@/libs/types/entities/Punishment";
5 |
6 |
7 | export const getPunishments = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
8 | await client.GetAsync(`/punishments/${uuid}`);
9 |
--------------------------------------------------------------------------------
/src/services/forum/search/SearchService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Perform a search based on the provided query.
5 | // *
6 | // * @param query The search query.
7 | // * @param limit The maximum number of results to return (default: 6).
8 | // * @return ResponseEntity with the search results in JSON format or an empty array if no results.
9 | // */
10 | //// In the API: @GetMapping(path = "/search")
11 | //export const Search = async (query: string, limit: number = 6, client?: HTTPClient) => {
12 | // const uri = `/search?query=${query}&limit=${limit}`;
13 | //
14 | // client ??= new HTTPClient(process.env.API_URL!);
15 | // const response = await (await client.GetAsync(uri)).json();
16 | //
17 | // return response;
18 | // }
--------------------------------------------------------------------------------
/src/services/forum/stats/GameStatsService.ts:
--------------------------------------------------------------------------------
1 | import HTTPClient from "@/libs/HTTPClient";
2 | import SkywarsStats from "@/libs/types/entities/SkywarsStats";
3 |
4 |
5 | export const getSkywarsStats = async (uuid: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
6 | await client.GetAsync(`/leaderboards/stats/skywars/${uuid}`);
7 |
--------------------------------------------------------------------------------
/src/services/forum/ticket/TicketCategoryService.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import HTTPClient from "@/libs/HTTPClient";
4 | import TicketCategory from "@/libs/types/entities/TicketCategory"
5 |
6 | /**
7 | * Create a new ticket category.
8 | *
9 | * @param category The TicketCategory object.
10 | * @return The created category.
11 | */
12 | // In the API: @PostMapping(path = "/forum/ticket/category")
13 | export const CreateTicketCategory = async (category: TicketCategory, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
14 | await client.PostAsync("/forum/ticket/category", category);
15 |
16 |
17 | /**
18 | * Edit an existing forum ticket category.
19 | *
20 | * @param category The TicketCategory object.
21 | * @return The edited category.
22 | */
23 | // In the API: @PutMapping(path = "/forum/ticket/category/{id}")
24 | export const EditTicketCategory = async (category: TicketCategory, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
25 | await client.PutAsync(`/forum/ticket/category/${category._id}`, category)
26 |
27 | /**
28 | * Get details of a specific forum ticket category by its ID.
29 | *
30 | * @param id The ID of the ticket category to retrieve.
31 | * @return The ResponseEntity with JSON representation of the ticket.
32 | */
33 | // In the API: @GetMapping(path = "/forum/ticket/category/{id}")
34 | export const GetTicketCategory = async (id: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
35 | await client.GetAsync(`/forum/ticket/category/${id}`)
36 |
37 | /**
38 | * Get all ticket categories.
39 | *
40 | * @return An array of categories.
41 | */
42 | // In the API: @GetMapping(path = "/forum/ticket/category")
43 | export const GetAllTicketCategories = async (client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
44 | await client.GetAsync(`/forum/ticket/category`)
45 |
46 |
--------------------------------------------------------------------------------
/src/services/forum/trophy/TrophyService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Get a list of all trophies.
5 | // *
6 | // * @return ResponseEntity containing a JsonArray with trophy information.
7 | // */
8 | //// In the API: @GetMapping(path = "/forum/trophy")
9 | //export const GetTrophies = async (client?: HTTPClient) => {
10 | // const uri = `/forum/trophy`;
11 | //
12 | // client ??= new HTTPClient(process.env.API_URL!);
13 | // const response = await (await client.GetAsync(uri)).json();
14 | //
15 | // return response;
16 | // }
17 | //
18 | ///**
19 | // * Delete a trophy by its ID.
20 | // *
21 | // * @param id The ID of the trophy to be deleted.
22 | // * @return ResponseEntity with the deleted trophy's JSON representation.
23 | // */
24 | // // In the API: @DeleteMapping(path = "/forum/trophy/{id}")
25 | //export const DeleteTrophy = async (id: string, client?: HTTPClient) => {
26 | // const uri = `/forum/trophy/${id}`;
27 | //
28 | // client ??= new HTTPClient(process.env.API_URL!);
29 | // const response = await (await client.DeleteAsync(uri)).json();
30 | //
31 | // return response;
32 | // }
33 | //
34 | ///**
35 | // * Create a new trophy.
36 | // *
37 | // * @param body (type: { id: string, name: string }) The JSON body containing information for creating the trophy.
38 | // * @return ResponseEntity with the created trophy's JSON representation.
39 | // */
40 | //// In the API: @PostMapping(path = "/forum/trophy")
41 | //export const CreateTrophy = async (formData: FormData, client?: HTTPClient) => {
42 | // const uri = `/forum/trophy`;
43 | //
44 | // client ??= new HTTPClient(process.env.API_URL!);
45 | // const response = await (await client.PostAsync(uri, formData)).json();
46 | //
47 | // return response;
48 | // }
49 | //
50 | ///**
51 | // * Get information about a specific trophy by its ID.
52 | // *
53 | // * @param id The ID of the trophy to retrieve.
54 | // * @return ResponseEntity with the trophy's JSON representation.
55 | // */
56 | //// In the API: @GetMapping(path = "/forum/trophy/{id}")
57 | //export const GetTrophy = async (id: string, client?: HTTPClient) => {
58 | // const uri = `/forum/trophy/${id}`;
59 | //
60 | // client ??= new HTTPClient(process.env.API_URL!);
61 | // const response = await (await client.GetAsync(uri)).json();
62 | //
63 | // return response;
64 | // }
--------------------------------------------------------------------------------
/src/services/forum/websiteData/WebsiteDataService.ts:
--------------------------------------------------------------------------------
1 | import HTTPClient from "@/libs/HTTPClient";
2 | import WebEntry from "@/libs/types/entities/WebEntry";
3 |
4 |
5 | export const getEntry = async (id: string, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
6 | (await client.GetAsync(`/websiteData/${id}`))[0]?.value;
7 |
8 | export const getAllEntries = async(client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
9 | (await client.GetAsync("/websiteData"))[0] || [];
10 |
11 | export const updateEntry = async (entry: WebEntry, client: HTTPClient = new HTTPClient(process.env.API_URL!)) =>
12 | await client.PostAsync(`/websiteData`, entry, true);
13 |
--------------------------------------------------------------------------------
/src/services/totp/TotpService.ts:
--------------------------------------------------------------------------------
1 | //'use server';
2 | //
3 | ///**
4 | // * Get TOTP data for a specific user.
5 | // *
6 | // * @param uuid The UUID of the user.
7 | // * @return ResponseEntity with the TOTP data in JSON format.
8 | // */
9 | //// In the API: @GetMapping(path = "/totp/{uuid}")
10 | //export const GetTotp = async (uuid: string, client?: HTTPClient) => {
11 | // const uri = `/totp/${uuid}`;
12 | //
13 | // client ??= new HTTPClient(process.env.API_URL!);
14 | // const response = await (await client.GetAsync(uri)).json();
15 | //
16 | // return response;
17 | // }
18 | //
19 | ///**
20 | // * Try to enable TOTP for a user using a provided code.
21 | // *
22 | // * @param body (type: { code: string }) The JSON body containing the TOTP code.
23 | // * @param uuid The UUID of the user.
24 | // * @return ResponseEntity with the result of the TOTP enablement attempt.
25 | // */
26 | //// In the API: @PostMapping(path = "/totp/{uuid}/tryEnable")
27 | //export const TryEnableTotp = async (formData: FormData, client?: HTTPClient) => {
28 | // const uuid = formData.get("uuid") as string;
29 | // const uri = `/totp/${uuid}/tryEnable`;
30 | //
31 | // client ??= new HTTPClient(process.env.API_URL!);
32 | // const response = await (await client.PostAsync(uri, formData)).json();
33 | //
34 | // return response;
35 | // }
36 | //
37 | ///**
38 | // * Try to authenticate a user using a provided TOTP code.
39 | // *
40 | // * @param body (type: { code: string }) The JSON body containing the TOTP code.
41 | // * @param uuid The UUID of the user.
42 | // * @return ResponseEntity with the result of the TOTP authentication attempt.
43 | // */
44 | //// In the API: @PostMapping(path = "/totp/{uuid}/tryAuth")
45 | //export const TryAuthTotp = async (formData: FormData, client?: HTTPClient) => {
46 | // const uuid = formData.get("uuid") as string;
47 | // const uri = `/totp/${uuid}/tryAuth`;
48 | //
49 | // client ??= new HTTPClient(process.env.API_URL!);
50 | // const response = await (await client.PostAsync(uri, formData)).json();
51 | //
52 | // return response;
53 | // }
54 | //
55 | ///**
56 | // * Delete TOTP data for a specific user.
57 | // *
58 | // * @param uuid The UUID of the user.
59 | // * @return ResponseEntity with the number of fields deleted.
60 | // */
61 | //// In the API: @DeleteMapping(path = "/totp/{uuid}")
62 | //export const DeleteTotp = async (uuid: string, client?: HTTPClient) => {
63 | // const uri = `/totp/${uuid}`;
64 | //
65 | // client ??= new HTTPClient(process.env.API_URL!);
66 | // const response = await (await client.DeleteAsync(uri)).json();
67 | //
68 | // return response;
69 | // }
70 | //
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noEmit": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "bundler",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "incremental": true,
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "paths": {
26 | "@/*": [
27 | "./src/*"
28 | ]
29 | }
30 | },
31 | "include": [
32 | "next-env.d.ts",
33 | "**/*.ts",
34 | "**/*.tsx",
35 | ".next/types/**/*.ts"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------