Compare commits
67 commits
e0319729c8
...
32d3549ef1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
32d3549ef1 | ||
![]() |
a5c8ba0e34 | ||
![]() |
6a1f3abaa5 | ||
![]() |
7ff860f3cc | ||
![]() |
d980d11be0 | ||
![]() |
7eb95b8ce0 | ||
![]() |
2d6fead452 | ||
![]() |
2f71aa12ae | ||
![]() |
28461e257c | ||
2a89f75ac6 | |||
0cb2a40eba | |||
![]() |
3ca54b39a1 | ||
![]() |
f6c1ce75a6 | ||
d8894cd0eb | |||
505fb56df8 | |||
74bc8014e9 | |||
![]() |
6791e36401 | ||
![]() |
63d7cd49d2 | ||
![]() |
42c730e073 | ||
![]() |
94a3d31e8d | ||
![]() |
5462af1a5a | ||
![]() |
a2307ec34e | ||
![]() |
0807ce5a67 | ||
![]() |
3b4f1f0476 | ||
f3007ed6cc | |||
1237e8a008 | |||
7f7736920a | |||
![]() |
6137049518 | ||
![]() |
8d23f58a10 | ||
e2a436bda3 | |||
![]() |
440b9d685d | ||
![]() |
aefb9e2018 | ||
![]() |
b11ad9354b | ||
![]() |
feb80916cc | ||
![]() |
3df39a4405 | ||
![]() |
7de5732596 | ||
![]() |
b816523cf9 | ||
![]() |
743c217ab2 | ||
![]() |
2325d648d9 | ||
![]() |
ce224af649 | ||
![]() |
373aff3911 | ||
![]() |
d9daea6ece | ||
![]() |
68b45a57b8 | ||
f26af125b8 | |||
![]() |
c8ccb82dce | ||
![]() |
8c611cbb86 | ||
![]() |
eb58c6c65a | ||
![]() |
166c9ac876 | ||
![]() |
095e8d73c0 | ||
![]() |
50ee8098c4 | ||
![]() |
469c16fac6 | ||
![]() |
3cc2d95007 | ||
![]() |
e5aa7c2a2a | ||
![]() |
32705c8662 | ||
![]() |
bc7ed52662 | ||
![]() |
7730f17490 | ||
![]() |
a258772f7e | ||
![]() |
54e5cb6495 | ||
296491241b | |||
![]() |
a3870224fa | ||
![]() |
e908237d84 | ||
![]() |
1280002a52 | ||
cef03390d4 | |||
![]() |
6d857ffc49 | ||
![]() |
606a0695d8 | ||
![]() |
e63f40c69b | ||
![]() |
2523a1052b |
21
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
|
||||
// https://github.com/csci430-os/vscode-remote-devcontainer/issues/2#issuecomment-1939950604
|
||||
"mounts": [
|
||||
"type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,readonly"
|
||||
]
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "yarn install",
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
18
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run dev",
|
||||
"runtimeExecutable": "next",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"dev"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
4138
package-lock.json
generated
45
package.json
|
@ -11,18 +11,61 @@
|
|||
"dependencies": {
|
||||
"@icons-pack/react-simple-icons": "^11.0.1",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tiptap/extension-blockquote": "^2.11.2",
|
||||
"@tiptap/extension-bold": "^2.11.2",
|
||||
"@tiptap/extension-bullet-list": "^2.11.2",
|
||||
"@tiptap/extension-character-count": "^2.11.2",
|
||||
"@tiptap/extension-code-block": "^2.11.2",
|
||||
"@tiptap/extension-document": "^2.11.2",
|
||||
"@tiptap/extension-dropcursor": "^2.11.2",
|
||||
"@tiptap/extension-hard-break": "^2.11.2",
|
||||
"@tiptap/extension-heading": "^2.11.2",
|
||||
"@tiptap/extension-highlight": "^2.11.2",
|
||||
"@tiptap/extension-history": "^2.11.2",
|
||||
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
||||
"@tiptap/extension-image": "^2.11.2",
|
||||
"@tiptap/extension-italic": "^2.11.2",
|
||||
"@tiptap/extension-list-item": "^2.11.2",
|
||||
"@tiptap/extension-ordered-list": "^2.11.2",
|
||||
"@tiptap/extension-paragraph": "^2.11.2",
|
||||
"@tiptap/extension-strike": "^2.11.2",
|
||||
"@tiptap/extension-subscript": "^2.11.2",
|
||||
"@tiptap/extension-superscript": "^2.11.2",
|
||||
"@tiptap/extension-table": "^2.11.2",
|
||||
"@tiptap/extension-table-cell": "^2.11.2",
|
||||
"@tiptap/extension-table-header": "^2.11.2",
|
||||
"@tiptap/extension-table-row": "^2.11.2",
|
||||
"@tiptap/extension-task-item": "^2.11.2",
|
||||
"@tiptap/extension-task-list": "^2.11.2",
|
||||
"@tiptap/extension-text": "^2.11.2",
|
||||
"@tiptap/extension-text-align": "^2.11.2",
|
||||
"@tiptap/extension-typography": "^2.11.2",
|
||||
"@tiptap/extension-underline": "^2.11.2",
|
||||
"@tiptap/extension-youtube": "^2.11.2",
|
||||
"@tiptap/pm": "^2.11.2",
|
||||
"@tiptap/react": "^2.11.2",
|
||||
"@tiptap/starter-kit": "^2.11.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.11.9",
|
||||
"i": "^0.3.7",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"next": "15.0.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"npm": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-toastify": "^11.0.3"
|
||||
"react-select": "^5.9.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"tiptap-markdown": "^0.8.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"postcss": "^8",
|
||||
|
|
BIN
public/images/D2J_Icon.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 25 KiB |
BIN
public/images/tags/art.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
public/images/tags/aseprite.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
public/images/tags/bevy.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
public/images/tags/bitsy.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/tags/blender.webp
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/tags/clickteam.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
public/images/tags/console-controller.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/images/tags/construct.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
public/images/tags/cryengine.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/images/tags/d2jam.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/images/tags/defold.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
public/images/tags/devlog.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
public/images/tags/digitalart.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/images/tags/flickgame.png
Normal file
After Width: | Height: | Size: 118 B |
BIN
public/images/tags/gameart.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/images/tags/gameassets.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
public/images/tags/gamedesign.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
public/images/tags/gamejam.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
public/images/tags/gamemaker.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
public/images/tags/gbstudio.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
public/images/tags/gdevelop.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
public/images/tags/glsl.png
Normal file
After Width: | Height: | Size: 63 KiB |
1
public/images/tags/godot.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="1024" width="1024" xmlns="http://www.w3.org/2000/svg"><path d="m0 0s-.325 1.994-.515 1.976l-36.182-3.491c-2.879-.278-5.115-2.574-5.317-5.459l-.994-14.247L-71-23.218l-1.904 12.912c-.424 2.872-2.932 5.037-5.835 5.037h-38.188c-2.902.0-5.41-2.165-5.834-5.037l-1.905-12.912-27.992 1.997-.994 14.247c-.202 2.886-2.438 5.182-5.317 5.46l-36.2 3.49c-.187.018-.324-1.978-.511-1.978l-.049-7.83 30.658-4.944 1.004-14.374c.203-2.91 2.551-5.263 5.463-5.472l38.551-2.75c.146-.01.29-.016.434-.016 2.897.0 5.401 2.166 5.825 5.038l1.959 13.286h28.005l1.959-13.286c.423-2.871 2.93-5.037 5.831-5.037.142.0.284.005.423.015l38.556 2.75c2.911.209 5.26 2.562 5.463 5.472l1.003 14.374 30.645 4.966z" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 919.24059 673.152141)"/><path d="m0 0v-47.514-6.035-5.492c.108-.001.216-.005.323-.015l36.196-3.49c1.896-.183 3.382-1.709 3.514-3.609l1.116-15.978 31.574-2.253 2.175 14.747c.282 1.912 1.922 3.329 3.856 3.329h38.188c1.933.0 3.573-1.417 3.855-3.329l2.175-14.747 31.575 2.253 1.115 15.978c.133 1.9 1.618 3.425 3.514 3.609l36.182 3.49c.107.01.214.014.322.015v4.711l.015.005v54.325c5.09692 6.4164715 9.92323 13.494208 13.621 19.449-5.651 9.62-12.575 18.217-19.976 26.182-6.864-3.455-13.531-7.369-19.828-11.534-3.151 3.132-6.7 5.694-10.186 8.372-3.425 2.751-7.285 4.768-10.946 7.118 1.09 8.117 1.629 16.108 1.846 24.448-9.446 4.754-19.519 7.906-29.708 10.17-4.068-6.837-7.788-14.241-11.028-21.479-3.842.642-7.702.88-11.567.926v.006c-.027.0-.052-.006-.075-.006-.024.0-.049.006-.073.006v-.006c-3.872-.046-7.729-.284-11.572-.926-3.238 7.238-6.956 14.642-11.03 21.479-10.184-2.264-20.258-5.416-29.703-10.17.216-8.34.755-16.331 1.848-24.448-3.668-2.35-7.523-4.367-10.949-7.118-3.481-2.678-7.036-5.24-10.188-8.372-6.297 4.165-12.962 8.079-19.828 11.534-7.401-7.965-14.321-16.562-19.974-26.182 4.4426579-6.973692 9.2079702-13.9828876 13.621-19.449z" fill="#478cbf" transform="matrix(4.162611 0 0 -4.162611 104.69892 427.387251)"/><path d="m0 0-1.121-16.063c-.135-1.936-1.675-3.477-3.611-3.616l-38.555-2.751c-.094-.007-.188-.01-.281-.01-1.916.0-3.569 1.406-3.852 3.33l-2.211 14.994H-81.09l-2.211-14.994c-.297-2.018-2.101-3.469-4.133-3.32l-38.555 2.751c-1.936.139-3.476 1.68-3.611 3.616L-130.721.0l-32.547 3.138c.015-3.498.06-7.33.06-8.093.0-34.374 43.605-50.896 97.781-51.086h.066.067c54.176.19 97.766 16.712 97.766 51.086.0.777.047 4.593.063 8.093z" fill="#478cbf" transform="matrix(4.162611 0 0 -4.162611 784.07144 718.723121)"/><path d="m0 0c0-12.052-9.765-21.815-21.813-21.815-12.042.0-21.81 9.763-21.81 21.815.0 12.044 9.768 21.802 21.81 21.802C-9.765 21.802.0 12.044.0.0" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 389.21484 527.151321)"/><path d="m0 0c0-7.994-6.479-14.473-14.479-14.473-7.996.0-14.479 6.479-14.479 14.473s6.483 14.479 14.479 14.479C-6.479 14.479.0 7.994.0.0" fill="#414042" transform="matrix(4.162611 0 0 -4.162611 367.36686 532.537071)"/><path d="m0 0c-3.878.0-7.021 2.858-7.021 6.381v20.081c0 3.52 3.143 6.381 7.021 6.381s7.028-2.861 7.028-6.381V6.381c0-3.523-3.15-6.381-7.028-6.381" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 511.99336 626.219821)"/><path d="m0 0c0-12.052 9.765-21.815 21.815-21.815 12.041.0 21.808 9.763 21.808 21.815.0 12.044-9.767 21.802-21.808 21.802-12.05.0-21.815-9.758-21.815-21.802" fill="#fff" transform="matrix(4.162611 0 0 -4.162611 634.78706 527.151321)"/><path d="m0 0c0-7.994 6.477-14.473 14.471-14.473C22.473-14.473 28.95-7.994 28.95.0s-6.477 14.479-14.479 14.479C6.477 14.479.0 7.994.0.0" fill="#414042" transform="matrix(4.162611 0 0 -4.162611 656.64056 532.537071)"/></svg>
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/images/tags/haxe.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/tags/indiedev.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/images/tags/itch.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
public/images/tags/libgdx.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
public/images/tags/love.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
public/images/tags/lowpoly.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
public/images/tags/meme.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
public/images/tags/music.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
public/images/tags/newspaper.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/images/tags/notebook.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/images/tags/pico8.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/tags/pixelart.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
public/images/tags/postmortem.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
public/images/tags/puzzlescript.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
public/images/tags/pygame.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
public/images/tags/renpy.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/images/tags/rpgmaker.png
Normal file
After Width: | Height: | Size: 357 KiB |
BIN
public/images/tags/rust.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
public/images/tags/saturday.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/images/tags/scratch.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/images/tags/shaders.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
public/images/tags/steam.png
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
public/images/tags/stencyl.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
public/images/tags/stream.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
public/images/tags/teamup.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
public/images/tags/tic80.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/tags/twine.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
public/images/tags/tyrano.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
public/images/tags/unity.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/tags/unreal.png
Normal file
After Width: | Height: | Size: 24 KiB |
3
src/app/about/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function AboutPage() {
|
||||
return <p>About page coming soon</p>;
|
||||
}
|
516
src/app/create-game/page.tsx
Normal file
|
@ -0,0 +1,516 @@
|
|||
"use client";
|
||||
|
||||
import Editor from "@/components/editor";
|
||||
import { getCookie } from "@/helpers/cookie";
|
||||
import { Button, Form, Input, Spacer } from "@nextui-org/react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { Select, SelectItem } from "@nextui-org/react";
|
||||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { GameType } from "@/types/GameType";
|
||||
import { PlatformType, DownloadLinkType } from "@/types/DownloadLinkType";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default function CreateGamePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [errors, setErrors] = useState({});
|
||||
const [waitingPost, setWaitingPost] = useState(false);
|
||||
const [editGame, setEditGame] = useState(false);
|
||||
/*
|
||||
const [selectedTags, setSelectedTags] = useState<MultiValue<{
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
isFixed: boolean;
|
||||
}> | null>(null);
|
||||
*/
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
|
||||
const [gameSlug, setGameSlug] = useState("");
|
||||
const [prevSlug, setPrevGameSlug] = useState("");
|
||||
const [game, setGame] = useState<GameType>();
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState("");
|
||||
const [authorSearch, setAuthorSearch] = useState("");
|
||||
const [selectedAuthors, setSelectedAuthors] = useState<Array<UserType>>([]);
|
||||
const [searchResults, setSearchResults] = useState<Array<UserType>>([]);
|
||||
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [downloadLinks, setDownloadLinks] = useState<DownloadLinkType[]>([]);
|
||||
const [editorKey, setEditorKey] = useState(0);
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
const urlRegex = /^(https?:\/\/)/;
|
||||
|
||||
const sanitizeSlug = (value: string): string => {
|
||||
return value
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.replace(/\s+/g, '-') // Replace whitespace with hyphens
|
||||
.replace(/[^a-z0-9-]/g, '') // Only allow lowercase letters, numbers, and hyphens
|
||||
.substring(0, 50); // Limit length to 50 characters
|
||||
};
|
||||
|
||||
const handleAuthorSearch = async (query: string) => {
|
||||
if (query.length < 3) return;
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/user/search?q=${query}`
|
||||
: `http://localhost:3005/api/v1/user/search?q=${query}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSearchResults(data);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
const load = async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
const localuser = await response.json();
|
||||
setUser(localuser);
|
||||
|
||||
/*
|
||||
const tagResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/tags`
|
||||
: `http://localhost:3005/api/v1/tags`
|
||||
);
|
||||
|
||||
if (tagResponse.ok) {
|
||||
const newoptions: {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
id: number;
|
||||
isFixed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (const tag of await tagResponse.json()) {
|
||||
if (tag.modOnly && localuser && !localuser.mod) {
|
||||
continue;
|
||||
}
|
||||
newoptions.push({
|
||||
value: tag.name,
|
||||
id: tag.id,
|
||||
label: (
|
||||
<div className="flex gap-2 items-center">
|
||||
{tag.icon && (
|
||||
<Avatar
|
||||
className="w-6 h-6 min-w-6 min-h-6"
|
||||
size="sm"
|
||||
src={tag.icon}
|
||||
classNames={{ base: "bg-transparent" }}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
{tag.name}
|
||||
{tag.modOnly ? " (Mod Only)" : ""}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
isFixed: tag.alwaysAdded,
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(newoptions);
|
||||
setSelectedTags(newoptions.filter((tag) => tag.isFixed));
|
||||
}
|
||||
*/
|
||||
};
|
||||
load();
|
||||
},[]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkExistingGame = async () => {
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
console.log("say");
|
||||
if (response.ok) {
|
||||
const gameData = await response.json();
|
||||
if (gameData) {
|
||||
setEditGame(true);
|
||||
setTitle(gameData.name);
|
||||
setGameSlug(gameData.slug);
|
||||
setPrevGameSlug(gameData.slug);
|
||||
setContent(gameData.description);
|
||||
setEditorKey((prev) => prev + 1);
|
||||
setThumbnailUrl(gameData.thumbnail);
|
||||
setDownloadLinks(gameData.downloadLinks);
|
||||
setGame(gameData);
|
||||
const uniqueAuthors = [gameData.author, ...gameData.contributors]
|
||||
.filter((author, index, self) =>
|
||||
index === self.findIndex((a) => a.id === author.id)
|
||||
);
|
||||
setSelectedAuthors(uniqueAuthors);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
setSelectedAuthors(user ? [user] : []);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
setEditGame(false);
|
||||
setTitle("");
|
||||
setGameSlug("");
|
||||
setContent("");
|
||||
setEditorKey((prev) => prev + 1);
|
||||
setThumbnailUrl("");
|
||||
setDownloadLinks([]);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
if (mounted && user) {
|
||||
checkExistingGame();
|
||||
}
|
||||
|
||||
},[user,mounted]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="static flex items-top mt-20 justify-center top-0 left-0">
|
||||
|
||||
<Form
|
||||
className="w-full max-w-2xl flex flex-col gap-4"
|
||||
validationErrors={errors}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!title && !content) {
|
||||
setErrors({
|
||||
title: "Please enter a valid title",
|
||||
content: "Please enter valid content",
|
||||
});
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
setErrors({ title: "Please enter a valid title" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
setErrors({ content: "Please enter valid content" });
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
const userSlug = getCookie("user"); // Retrieve user slug from cookies
|
||||
if (!userSlug) {
|
||||
toast.error("You are not logged in.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
try {
|
||||
const requestMethod = editGame ? "PUT" : "POST";
|
||||
const endpoint = editGame ? `/games/${prevSlug}` : "/games/create";
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1${endpoint}`
|
||||
: `http://localhost:3005/api/v1${endpoint}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
name: title,
|
||||
slug: gameSlug,
|
||||
description: sanitizedHtml,
|
||||
thumbnail: thumbnailUrl,
|
||||
downloadLinks: downloadLinks.map((link) => ({
|
||||
url: link.url,
|
||||
platform: link.platform,
|
||||
})),
|
||||
userSlug,
|
||||
contributors: selectedAuthors.map((author) => author.id),
|
||||
}),
|
||||
method: requestMethod,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
setErrors({ content: "Invalid user" });
|
||||
setWaitingPost(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(gameSlug ? "Game updated successfully!" : "Game created successfully!");
|
||||
setWaitingPost(false);
|
||||
router.push(`/games/${gameSlug || sanitizeSlug(title)}`);
|
||||
} else {
|
||||
const error = await response.text();
|
||||
toast.error(error || "Failed to create game");
|
||||
setWaitingPost(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating game:", error);
|
||||
toast.error("Failed to create game.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4 flex">
|
||||
{gameSlug ? "Edit Game" : "Create New Game"}
|
||||
</h1>
|
||||
</div>
|
||||
<Input
|
||||
isRequired
|
||||
label="Game Name"
|
||||
labelPlacement="outside"
|
||||
name="title"
|
||||
placeholder="Enter your game name"
|
||||
type="text"
|
||||
value={title}
|
||||
onValueChange={(value) => {
|
||||
setTitle(value);
|
||||
if (!isSlugManuallyEdited) {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Game Slug"
|
||||
labelPlacement="outside"
|
||||
placeholder="your-game-name"
|
||||
value={gameSlug}
|
||||
onValueChange={(value) => {
|
||||
setGameSlug(sanitizeSlug(value));
|
||||
setIsSlugManuallyEdited(true);
|
||||
}}
|
||||
description="This will be used in the URL: d2jam.com/games/your-game-name"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Add Authors</label>
|
||||
<Input
|
||||
placeholder="Search users..."
|
||||
value={authorSearch}
|
||||
onValueChange={(value) => {
|
||||
setAuthorSearch(value);
|
||||
handleAuthorSearch(value);
|
||||
}}
|
||||
/>
|
||||
{searchResults.length > 0 && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-2">
|
||||
{searchResults.map((user) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className="flex justify-between items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer"
|
||||
onClick={() => {
|
||||
if (!selectedAuthors.some(a => a.id === user.id)) {
|
||||
setSelectedAuthors([...selectedAuthors, user]);
|
||||
}
|
||||
setSearchResults([]);
|
||||
setAuthorSearch("");
|
||||
}}
|
||||
>
|
||||
<span>{user.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{selectedAuthors.map((author) => (
|
||||
<div
|
||||
key={author.id}
|
||||
className="bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-100 px-3 py-1 rounded-full flex items-center gap-2"
|
||||
>
|
||||
<span>{author.name}</span>
|
||||
{((game && author.id !== game.authorId) || (!game && author.id !== user?.id)) && (
|
||||
<button
|
||||
onClick={() => setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id))}
|
||||
className="text-sm hover:text-red-500"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<label className="text-sm font-medium">Game Description</label>
|
||||
<Editor key={editorKey} content={content} setContent={setContent} gameEditor />
|
||||
|
||||
<Spacer />
|
||||
|
||||
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
|
||||
<Input
|
||||
label="Thumbnail URL"
|
||||
labelPlacement="outside"
|
||||
placeholder="https://example.com/thumbnail.png"
|
||||
value={thumbnailUrl}
|
||||
onValueChange={setThumbnailUrl}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
{Array.isArray(downloadLinks) &&
|
||||
downloadLinks.map((link, index) => (
|
||||
<div key={link.id} className="flex gap-2">
|
||||
<Input
|
||||
className="flex-grow"
|
||||
placeholder="https://example.com/download"
|
||||
value={link.url}
|
||||
onValueChange={(value) => {
|
||||
const newLinks = [...downloadLinks];
|
||||
newLinks[index].url = value;
|
||||
setDownloadLinks(newLinks);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!urlRegex.test(downloadLinks[index].url)) {
|
||||
toast.error("Please enter a valid URL starting with http:// or https://");
|
||||
|
||||
if (!downloadLinks[index].url.startsWith("http://") && !downloadLinks[index].url.startsWith("https://")) {
|
||||
const newUrl = "https://" + downloadLinks[index].url;
|
||||
const newLinks = [...downloadLinks];
|
||||
newLinks[index].url = newUrl;
|
||||
setDownloadLinks(newLinks);
|
||||
const input = document.querySelector<HTMLInputElement>(
|
||||
`#download-link-${index}`
|
||||
);
|
||||
if (input) {
|
||||
input.value = newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
className="w-96"
|
||||
defaultSelectedKeys={["Windows"]}
|
||||
aria-label="Select platform" // Add this to fix accessibility warning
|
||||
onSelectionChange={(value) => {
|
||||
const newLinks = [...downloadLinks];
|
||||
newLinks[index].platform = value as unknown as PlatformType;
|
||||
setDownloadLinks(newLinks);
|
||||
}}
|
||||
>
|
||||
<SelectItem key="Windows" value="Windows">
|
||||
Windows
|
||||
</SelectItem>
|
||||
<SelectItem key="MacOS" value="MacOS">
|
||||
MacOS
|
||||
</SelectItem>
|
||||
<SelectItem key="Linux" value="Linux">
|
||||
Linux
|
||||
</SelectItem>
|
||||
<SelectItem key="Web" value="Web">
|
||||
Web
|
||||
</SelectItem>
|
||||
<SelectItem key="Mobile" value="Mobile">
|
||||
Mobile
|
||||
</SelectItem>
|
||||
<SelectItem key="Other" value="Other">
|
||||
Other
|
||||
</SelectItem>
|
||||
</Select>
|
||||
<Button
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
setDownloadLinks(downloadLinks.filter((l) => l.id !== link.id));
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={() => {
|
||||
setDownloadLinks([
|
||||
...downloadLinks,
|
||||
{
|
||||
id: Date.now(),
|
||||
url: "",
|
||||
platform: "Windows",
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
Add Download Link
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>{editGame ? "Update" : "Create"}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col gap-4 px-8 items-end">
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,25 +1,190 @@
|
|||
"use client";
|
||||
|
||||
import Editor from "@/components/editor";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { Button, Form, Input, Textarea } from "@nextui-org/react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
Spacer,
|
||||
} from "@nextui-org/react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import Select, { MultiValue, StylesConfig } from "react-select";
|
||||
import { useTheme } from "next-themes";
|
||||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import { UserType } from "@/types/UserType";
|
||||
|
||||
export default function CreatePostPage() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [errors, setErrors] = useState({});
|
||||
const [waitingPost, setWaitingPost] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<MultiValue<{
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
isFixed: boolean;
|
||||
}> | null>(null);
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
const [options, setOptions] = useState<
|
||||
{
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
id: number;
|
||||
isFixed: boolean;
|
||||
}[]
|
||||
>();
|
||||
const { theme } = useTheme();
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [sticky, setSticky] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
const load = async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
const localuser = await response.json();
|
||||
setUser(localuser);
|
||||
|
||||
const tagResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/tags`
|
||||
: `http://localhost:3005/api/v1/tags`
|
||||
);
|
||||
|
||||
if (tagResponse.ok) {
|
||||
const newoptions: {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
id: number;
|
||||
isFixed: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (const tag of await tagResponse.json()) {
|
||||
if (tag.modOnly && !localuser.mod) {
|
||||
continue;
|
||||
}
|
||||
newoptions.push({
|
||||
value: tag.name,
|
||||
id: tag.id,
|
||||
label: (
|
||||
<div className="flex gap-2 items-center">
|
||||
{tag.icon && (
|
||||
<Avatar
|
||||
className="w-6 h-6 min-w-6 min-h-6"
|
||||
size="sm"
|
||||
src={tag.icon}
|
||||
classNames={{ base: "bg-transparent" }}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
{tag.name}
|
||||
{tag.modOnly ? " (Mod Only)" : ""}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
isFixed: tag.alwaysAdded,
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(newoptions);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
|
||||
const styles: StylesConfig<
|
||||
{
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
isFixed: boolean;
|
||||
},
|
||||
true
|
||||
> = {
|
||||
multiValue: (base, state) => {
|
||||
return {
|
||||
...base,
|
||||
backgroundColor: state.data.isFixed
|
||||
? theme == "dark"
|
||||
? "#222"
|
||||
: "#ddd"
|
||||
: theme == "dark"
|
||||
? "#444"
|
||||
: "#eee",
|
||||
};
|
||||
},
|
||||
multiValueLabel: (base, state) => {
|
||||
return {
|
||||
...base,
|
||||
color: state.data.isFixed
|
||||
? theme == "dark"
|
||||
? "#ddd"
|
||||
: "#222"
|
||||
: theme == "dark"
|
||||
? "#fff"
|
||||
: "#444",
|
||||
fontWeight: state.data.isFixed ? "normal" : "bold",
|
||||
paddingRight: state.data.isFixed ? "8px" : "2px",
|
||||
};
|
||||
},
|
||||
multiValueRemove: (base, state) => {
|
||||
return {
|
||||
...base,
|
||||
display: state.data.isFixed ? "none" : "flex",
|
||||
color: theme == "dark" ? "#ddd" : "#222",
|
||||
};
|
||||
},
|
||||
control: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: theme == "dark" ? "#181818" : "#fff",
|
||||
minWidth: "300px",
|
||||
}),
|
||||
menu: (styles) => ({
|
||||
...styles,
|
||||
backgroundColor: theme == "dark" ? "#181818" : "#fff",
|
||||
color: theme == "dark" ? "#fff" : "#444",
|
||||
}),
|
||||
option: (styles, { isFocused }) => ({
|
||||
...styles,
|
||||
backgroundColor: isFocused
|
||||
? theme == "dark"
|
||||
? "#333"
|
||||
: "#ddd"
|
||||
: undefined,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
|
||||
<div className="absolute flex items-top mt-40 justify-center top-0 left-0 w-screen h-screen">
|
||||
<Form
|
||||
className="w-full max-w-xs flex flex-col gap-4"
|
||||
className="w-full max-w-2xl flex flex-col gap-4"
|
||||
validationErrors={errors}
|
||||
onReset={() => {
|
||||
setTitle("");
|
||||
setContent("");
|
||||
}}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -28,6 +193,7 @@ export default function CreatePostPage() {
|
|||
title: "Please enter a valid title",
|
||||
content: "Please enter valid content",
|
||||
});
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -38,6 +204,7 @@ export default function CreatePostPage() {
|
|||
|
||||
if (!content) {
|
||||
setErrors({ content: "Please enter valid content" });
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -46,6 +213,19 @@ export default function CreatePostPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const tags = [];
|
||||
|
||||
if (selectedTags) {
|
||||
for (const tag of selectedTags) {
|
||||
tags.push(
|
||||
options?.filter((option) => option.value == tag.value)[0].id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post"
|
||||
|
@ -53,25 +233,39 @@ export default function CreatePostPage() {
|
|||
{
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
content: content,
|
||||
content: sanitizedHtml,
|
||||
sticky,
|
||||
username: getCookie("user"),
|
||||
tags: [
|
||||
...tags,
|
||||
...(options
|
||||
? options.filter((tag) => tag.isFixed).map((tag) => tag.id)
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status == 401) {
|
||||
setErrors({ content: "Invalid user" });
|
||||
setWaitingPost(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Successfully created post");
|
||||
|
||||
redirect("/");
|
||||
if (response.ok) {
|
||||
toast.success("Successfully created post");
|
||||
setWaitingPost(false);
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
setWaitingPost(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
@ -85,24 +279,51 @@ export default function CreatePostPage() {
|
|||
onValueChange={setTitle}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
isRequired
|
||||
label="Content"
|
||||
labelPlacement="outside"
|
||||
name="content"
|
||||
placeholder="Enter the post body"
|
||||
value={content}
|
||||
onValueChange={setContent}
|
||||
/>
|
||||
<Editor content={content} setContent={setContent} />
|
||||
|
||||
<Spacer />
|
||||
|
||||
{mounted && (
|
||||
<Select
|
||||
styles={styles}
|
||||
isMulti
|
||||
value={selectedTags}
|
||||
onChange={(value) => setSelectedTags(value)}
|
||||
options={options}
|
||||
isClearable={false}
|
||||
isOptionDisabled={() =>
|
||||
selectedTags != null && selectedTags.length >= 5
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{user && user.mod && (
|
||||
<div>
|
||||
<Spacer />
|
||||
<Checkbox isSelected={sticky} onValueChange={setSticky}>
|
||||
Sticky
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
Create
|
||||
</Button>
|
||||
<Button type="reset" variant="flat">
|
||||
Reset
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>Create</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col gap-4 px-8 items-end">
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
163
src/app/games/[gameSlug]/page.tsx
Normal file
|
@ -0,0 +1,163 @@
|
|||
"use client";
|
||||
|
||||
import { use } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getCookie } from '@/helpers/cookie';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@nextui-org/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { GameType } from '@/types/GameType';
|
||||
import { UserType } from '@/types/UserType';
|
||||
import { DownloadLinkType } from '@/types/DownloadLinkType';
|
||||
|
||||
export default function GamePage({ params }: { params: Promise<{ gameSlug: string }> }) {
|
||||
const resolvedParams = use(params);
|
||||
const gameSlug = resolvedParams.gameSlug;
|
||||
const [game, setGame] = useState<GameType | null>(null);
|
||||
const [user, setUser] = useState<UserType | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchGameAndUser = async () => {
|
||||
// Fetch the game data
|
||||
const gameResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/games/${gameSlug}`
|
||||
: `http://localhost:3005/api/v1/games/${gameSlug}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (gameResponse.ok) {
|
||||
const gameData = await gameResponse.json();
|
||||
|
||||
const filteredContributors = gameData.contributors.filter(
|
||||
(contributor: UserType) => contributor.id !== gameData.author.id
|
||||
);
|
||||
|
||||
const updatedGameData = {
|
||||
...gameData,
|
||||
contributors: filteredContributors,
|
||||
};
|
||||
|
||||
setGame(updatedGameData);
|
||||
}
|
||||
|
||||
// Fetch the logged-in user data
|
||||
if (getCookie("token")) {
|
||||
const userResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUser(userData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchGameAndUser();
|
||||
}, [gameSlug]);
|
||||
|
||||
if (!game) return <div>Loading...</div>;
|
||||
|
||||
// Check if the logged-in user is the creator or a contributor
|
||||
const isEditable =
|
||||
user &&
|
||||
(user.id === game.author.id ||
|
||||
game.contributors.some((contributor: UserType) => contributor.id === user.id));
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Game Name and Edit Button */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-4xl font-bold">{game.name}</h1>
|
||||
{isEditable && (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="solid"
|
||||
onPress={() => router.push(`/create-game`)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authors */}
|
||||
<div className="mb-8">
|
||||
<p className="text-gray-600">
|
||||
Created by{' '}
|
||||
<Link href={`/users/${game.author.slug}`} className="text-blue-500 hover:underline">
|
||||
{game.author.name}
|
||||
</Link>
|
||||
{game.contributors.length > 0 && (
|
||||
<>
|
||||
{' '}with{' '}
|
||||
{game.contributors.map((contributor: UserType, index: number) => (
|
||||
<span key={contributor.id}>
|
||||
<Link href={`/users/${contributor.slug}`} className="text-blue-500 hover:underline">
|
||||
{contributor.name}
|
||||
</Link>
|
||||
{index < game.contributors.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-semibold mb-4">About</h2>
|
||||
<div className="prose-neutral prose-lg" dangerouslySetInnerHTML={{ __html: game.description ?? '' }} />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-semibold mb-4">Downloads</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{game.downloadLinks.map((link: DownloadLinkType) => (
|
||||
<Button
|
||||
key={link.id}
|
||||
as="a"
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full"
|
||||
color="primary"
|
||||
variant="bordered"
|
||||
>
|
||||
Download for {link.platform}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Metrics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Views</p>
|
||||
<p className="text-2xl font-bold">0</p>
|
||||
</div>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Downloads</p>
|
||||
<p className="text-2xl font-bold">0</p>
|
||||
</div>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Rating</p>
|
||||
<p className="text-2xl font-bold">N/A</p>
|
||||
</div>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Comments</p>
|
||||
<p className="text-2xl font-bold">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
3
src/app/games/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function GamesPage() {
|
||||
return <p>Games page coming soon</p>;
|
||||
}
|
3
src/app/inbox/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function InboxPage() {
|
||||
return <p>Inbox page coming soon</p>;
|
||||
}
|
|
@ -2,13 +2,15 @@ import type { Metadata } from "next";
|
|||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "../components/navbar";
|
||||
import Providers from "./providers";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { NextUIProvider, Spacer } from "@nextui-org/react";
|
||||
import Footer from "@/components/footer";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dare2Jam",
|
||||
title: "Down2Jam",
|
||||
description: "A community built game jam!",
|
||||
};
|
||||
|
||||
|
@ -18,17 +20,23 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>
|
||||
<div className="dark">
|
||||
<div className="bg-zinc-100 dark:bg-zinc-950 min-h-screen">
|
||||
<Navbar />
|
||||
<div className="max-w-8xl mx-auto">{children}</div>
|
||||
<ToastContainer />
|
||||
<NextUIProvider>
|
||||
<ThemeProvider attribute="class">
|
||||
<div className="">
|
||||
<div className="bg-[#fff] dark:bg-[#181818] min-h-screen flex flex-col ease-in-out transition-color duration-500">
|
||||
<Navbar />
|
||||
<Spacer y={5} />
|
||||
<div className="max-w-6xl xl:max-w-7xl 2xl:max-w-8xl mx-auto flex-grow w-full">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Providers>
|
||||
</ThemeProvider>
|
||||
</NextUIProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Form, Input } from "@nextui-org/react";
|
||||
import { Button, Form, Input, Link } from "@nextui-org/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
@ -48,6 +48,7 @@ export default function UserPage() {
|
|||
body: JSON.stringify({ username: username, password: password }),
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -57,7 +58,14 @@ export default function UserPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const { token, user } = await response.json();
|
||||
const { user } = await response.json();
|
||||
const token = response.headers.get("Authorization");
|
||||
|
||||
if (!token) {
|
||||
toast.error("Failed to retreive access token");
|
||||
setPassword("");
|
||||
return;
|
||||
}
|
||||
|
||||
document.cookie = `token=${token}`;
|
||||
document.cookie = `user=${user.slug}`;
|
||||
|
@ -96,6 +104,9 @@ export default function UserPage() {
|
|||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[#333] dark:text-white transition-color duration-250">
|
||||
Don't have an account? <Link href="/signup">Sign up</Link>
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,11 +6,28 @@ import { toast } from "react-toastify";
|
|||
|
||||
export default function UserPage() {
|
||||
useEffect(() => {
|
||||
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
async function logout() {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/logout"
|
||||
: "http://localhost:3005/api/v1/logout",
|
||||
{ method: "POST", credentials: "include" }
|
||||
);
|
||||
|
||||
toast.success("Successfully logged out");
|
||||
if (response.ok) {
|
||||
document.cookie =
|
||||
"token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
document.cookie =
|
||||
"user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
|
||||
redirect("/");
|
||||
toast.success("Successfully logged out");
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("Error while trying to log out");
|
||||
}
|
||||
}
|
||||
|
||||
logout();
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
639
src/app/p/[slug]/page.tsx
Normal file
|
@ -0,0 +1,639 @@
|
|||
"use client";
|
||||
|
||||
import LikeButton from "@/components/posts/LikeButton";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { PostType } from "@/types/PostType";
|
||||
import { TagType } from "@/types/TagType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Chip,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownSection,
|
||||
DropdownTrigger,
|
||||
Spacer,
|
||||
} from "@nextui-org/react";
|
||||
import { formatDistance } from "date-fns";
|
||||
import {
|
||||
Flag,
|
||||
LoaderCircle,
|
||||
MessageCircle,
|
||||
MoreVertical,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
ShieldX,
|
||||
Star,
|
||||
StarOff,
|
||||
Trash,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { redirect, useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import Editor from "@/components/editor";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import CommentCard from "@/components/posts/CommentCard";
|
||||
|
||||
export default function PostPage() {
|
||||
const [post, setPost] = useState<PostType>();
|
||||
const { slug } = useParams();
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [content, setContent] = useState("");
|
||||
const [waitingPost, setWaitingPost] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserAndPosts = async () => {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch the user
|
||||
const userResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (userResponse.ok) {
|
||||
const userData = await userResponse.json();
|
||||
setUser(userData);
|
||||
|
||||
const postResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/post?slug=${slug}&user=${userData.slug}`
|
||||
: `http://localhost:3005/api/v1/post?slug=${slug}&user=${userData.slug}`
|
||||
);
|
||||
setPost(await postResponse.json());
|
||||
setLoading(false);
|
||||
} else {
|
||||
setUser(undefined);
|
||||
|
||||
const postResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/post?slug=${slug}`
|
||||
: `http://localhost:3005/api/v1/post?slug=${slug}`
|
||||
);
|
||||
setPost(await postResponse.json());
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserAndPosts();
|
||||
}, [slug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<div className="flex justify-center p-6">
|
||||
<LoaderCircle
|
||||
className="animate-spin text-[#333] dark:text-[#999]"
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all">
|
||||
<CardBody className="p-5">
|
||||
<div>
|
||||
{post && (
|
||||
<div>
|
||||
<Link href={`/p/${post.slug}`}>
|
||||
<p className="text-2xl">{post.title}</p>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
|
||||
<p>By</p>
|
||||
<Link
|
||||
href={`/u/${post.author.slug}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
className="w-6 h-6"
|
||||
src={post.author.profilePicture}
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
<p>{post.author.name}</p>
|
||||
</Link>
|
||||
<p>
|
||||
{formatDistance(new Date(post.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
<div
|
||||
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
|
||||
<Spacer y={4} />
|
||||
|
||||
{post.tags.filter((tag) => tag.name != "D2Jam").length >
|
||||
0 ? (
|
||||
<div className="flex gap-1">
|
||||
{post.tags
|
||||
.filter((tag) => tag.name != "D2Jam")
|
||||
.map((tag: TagType) => (
|
||||
<Link
|
||||
href="/"
|
||||
key={tag.id}
|
||||
className={`transition-all transform duration-500 ease-in-out ${
|
||||
!reduceMotion ? "hover:scale-110" : ""
|
||||
}`}
|
||||
>
|
||||
<Chip
|
||||
radius="sm"
|
||||
size="sm"
|
||||
className="!duration-250 !ease-linear !transition-all"
|
||||
variant="faded"
|
||||
avatar={
|
||||
tag.icon && (
|
||||
<Avatar
|
||||
src={tag.icon}
|
||||
classNames={{ base: "bg-transparent" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{tag.name}
|
||||
</Chip>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{post.tags.length > 0 && <Spacer y={4} />}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<LikeButton
|
||||
likes={post.likes.length}
|
||||
liked={post.hasLiked}
|
||||
parentId={post.id}
|
||||
/>
|
||||
<Link href="#create-comment">
|
||||
<Button size="sm" variant="bordered">
|
||||
<MessageCircle size={16} /> {post.comments.length}
|
||||
</Button>
|
||||
</Link>
|
||||
<Dropdown backdrop="opaque">
|
||||
<DropdownTrigger>
|
||||
<Button size="sm" variant="bordered" isIconOnly>
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu className="text-[#333] dark:text-white">
|
||||
<DropdownSection
|
||||
showDivider={user?.mod}
|
||||
title="Actions"
|
||||
>
|
||||
<DropdownItem
|
||||
key="report"
|
||||
startContent={<Flag />}
|
||||
description="Report this post to moderators to handle"
|
||||
onPress={() => {
|
||||
toast.warning(
|
||||
"Report functionality coming soon"
|
||||
);
|
||||
}}
|
||||
>
|
||||
Create Report
|
||||
</DropdownItem>
|
||||
{user?.slug == post.author.slug ? (
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
startContent={<Trash />}
|
||||
description="Delete your post"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post"
|
||||
: "http://localhost:3005/api/v1/post",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
postId: post.id,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Deleted post");
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("Error while deleting post");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</DropdownSection>
|
||||
{user?.mod ? (
|
||||
<DropdownSection title="Mod Zone">
|
||||
<DropdownItem
|
||||
key="remove"
|
||||
startContent={<X />}
|
||||
description="Remove this post"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post"
|
||||
: "http://localhost:3005/api/v1/post",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
postId: post.id,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Removed post");
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("Error while removing post");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</DropdownItem>
|
||||
{post.sticky ? (
|
||||
<DropdownItem
|
||||
key="unsticky"
|
||||
startContent={<StarOff />}
|
||||
description="Unsticky post"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post/sticky"
|
||||
: "http://localhost:3005/api/v1/post/sticky",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
postId: post.id,
|
||||
sticky: false,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Unsticked post");
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("Error while removing post");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unsticky
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<DropdownItem
|
||||
key="sticky"
|
||||
startContent={<Star />}
|
||||
description="Sticky post"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post/sticky"
|
||||
: "http://localhost:3005/api/v1/post/sticky",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
postId: post.id,
|
||||
sticky: true,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Unsticked post");
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("Error while removing post");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sticky
|
||||
</DropdownItem>
|
||||
)}
|
||||
{user?.admin && !post.author.mod ? (
|
||||
<DropdownItem
|
||||
key="promote-mod"
|
||||
startContent={<Shield />}
|
||||
description="Promote user to Mod"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/mod"
|
||||
: "http://localhost:3005/api/v1/mod",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
targetname: post.author.slug,
|
||||
mod: true,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Promoted User to Mod");
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error while promoting user to Mod"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Appoint as mod
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{user?.admin &&
|
||||
post.author.mod &&
|
||||
!post.author.admin ? (
|
||||
<DropdownItem
|
||||
key="demote-mod"
|
||||
startContent={<ShieldX />}
|
||||
description="Demote user from Mod"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/mod"
|
||||
: "http://localhost:3005/api/v1/mod",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
targetname: post.author.slug,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Demoted User");
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error("Error while demoting user");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove as mod
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{user?.admin && !post.author.admin ? (
|
||||
<DropdownItem
|
||||
key="promote-admin"
|
||||
startContent={<ShieldAlert />}
|
||||
description="Promote user to Admin"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/mod"
|
||||
: "http://localhost:3005/api/v1/mod",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
targetname: post.author.slug,
|
||||
admin: true,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Promoted User to Admin");
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error while promoting user to Admin"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Appoint as admin
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{user?.admin &&
|
||||
post.author.admin &&
|
||||
post.author.id !== user.id ? (
|
||||
<DropdownItem
|
||||
key="demote-admin"
|
||||
startContent={<ShieldX />}
|
||||
description="Demote user to mod"
|
||||
onPress={async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/mod"
|
||||
: "http://localhost:3005/api/v1/mod",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
targetname: post.author.slug,
|
||||
mod: true,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie(
|
||||
"token"
|
||||
)}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Demoted User to Mod");
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error while demoting user to mod"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove as admin
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</DropdownSection>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div id="create-comment" />
|
||||
<Spacer y={10} />
|
||||
<Editor content={content} setContent={setContent} />
|
||||
<Spacer />
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
if (!content) {
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasCookie("token")) {
|
||||
toast.error("You are not logged in");
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/comment"
|
||||
: "http://localhost:3005/api/v1/comment",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
content: sanitizedHtml,
|
||||
username: getCookie("user"),
|
||||
postId: post?.id,
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status == 401) {
|
||||
toast.error("Invalid User");
|
||||
setWaitingPost(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Successfully created comment");
|
||||
setWaitingPost(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
setWaitingPost(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>Create Comment</p>
|
||||
)}
|
||||
</Button>
|
||||
<Spacer y={10} />
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{post?.comments.map((comment) => (
|
||||
<div key={comment.id}>
|
||||
<CommentCard comment={comment} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,46 +1,18 @@
|
|||
import { Image } from "@nextui-org/image";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import { SiDiscord } from "@icons-pack/react-simple-icons";
|
||||
import { Link } from "@nextui-org/react";
|
||||
import Posts from "@/components/posts";
|
||||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import JamHeader from "@/components/jam-header";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="fixed left-0 top-0 w-full h-full z-0">
|
||||
<Image
|
||||
src="/images/bg.jpg"
|
||||
alt="Home background"
|
||||
className="object-cover w-full h-full"
|
||||
radius="none"
|
||||
loading="eager"
|
||||
removeWrapper
|
||||
/>
|
||||
<div className="absolute left-0 top-0 w-full h-full bg-gradient-to-r from-black/50 to-transparent z-10" />
|
||||
<div className="flex justify-between flex-wrap">
|
||||
<div className="md:w-2/3">
|
||||
<JamHeader />
|
||||
<Posts />
|
||||
</div>
|
||||
<div className="z-10 relative flex w-full flex-wrap">
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 py-4 sm:py-8 md:py-12 pl-16">
|
||||
<h1 className="text-3xl sm:text-4xl md:text-5xl">Dare2Jam</h1>
|
||||
<p className="text-lg sm:text-xl">April 4th - 7th</p>
|
||||
<div className="flex gap-2">
|
||||
<Link href="https://discord.gg/rfmKzM6ASw" target="_blank">
|
||||
<Button
|
||||
variant="bordered"
|
||||
className="border-white/50 text-white"
|
||||
startContent={<SiDiscord />}
|
||||
>
|
||||
Join the Discord
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Posts />
|
||||
</div>
|
||||
<div className="w-1/3 flex justify-end py-4 sm:py-8 md:py-12 flex-grow">
|
||||
<Timers />
|
||||
</div>
|
||||
<div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
export default function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<NextUIProvider>
|
||||
{children}
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
3
src/app/reports/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function ReportsPage() {
|
||||
return <p>Reports page coming soon</p>;
|
||||
}
|
179
src/app/settings/page.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
"use client";
|
||||
|
||||
import Editor from "@/components/editor";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { Avatar, Button, Form, Input } from "@nextui-org/react";
|
||||
import { redirect, usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function UserPage() {
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [profilePicture, setProfilePicture] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [bannerPicture, setBannerPicture] = useState("");
|
||||
const [bio, setBio] = useState("");
|
||||
const [errors] = useState({});
|
||||
const pathname = usePathname();
|
||||
const [waitingSave, setWaitingSave] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
async function loadUser() {
|
||||
if (!hasCookie("token")) {
|
||||
setUser(undefined);
|
||||
redirect("/");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status == 200) {
|
||||
const data = await response.json();
|
||||
setUser(data);
|
||||
|
||||
setProfilePicture(data.profilePicture ?? "");
|
||||
setBannerPicture(data.bannerPicture ?? "");
|
||||
setBio(data.bio ?? "");
|
||||
setName(data.name ?? "");
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
{!user ? (
|
||||
"Loading settings..."
|
||||
) : (
|
||||
<Form
|
||||
className="w-full max-w-2xl flex flex-col gap-4"
|
||||
validationErrors={errors}
|
||||
onReset={() => {
|
||||
setProfilePicture(user.profilePicture ?? "");
|
||||
setBannerPicture(user.bannerPicture ?? "");
|
||||
setBio(user.bio ?? "");
|
||||
setName(user.name ?? "");
|
||||
}}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const sanitizedBio = sanitizeHtml(bio);
|
||||
|
||||
if (!name) {
|
||||
toast.error("You need to enter a name");
|
||||
return;
|
||||
}
|
||||
|
||||
setWaitingSave(true);
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/user"
|
||||
: "http://localhost:3005/api/v1/user",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
slug: user.slug,
|
||||
name: name,
|
||||
bio: sanitizedBio,
|
||||
profilePicture: profilePicture,
|
||||
bannerPicture: bannerPicture,
|
||||
}),
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Changed settings");
|
||||
setUser(await response.json());
|
||||
setWaitingSave(false);
|
||||
} else {
|
||||
toast.error("Failed to update settings");
|
||||
setWaitingSave(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-3xl">Settings</p>
|
||||
|
||||
<Input
|
||||
label="Name"
|
||||
labelPlacement="outside"
|
||||
name="name"
|
||||
placeholder="Enter a name"
|
||||
type="text"
|
||||
value={name}
|
||||
onValueChange={setName}
|
||||
/>
|
||||
|
||||
<p>Bio</p>
|
||||
<Editor content={bio} setContent={setBio} />
|
||||
|
||||
<Input
|
||||
label="Profile Picture"
|
||||
labelPlacement="outside"
|
||||
name="profilePicture"
|
||||
placeholder="Enter a url to an image"
|
||||
type="text"
|
||||
value={profilePicture}
|
||||
onValueChange={setProfilePicture}
|
||||
/>
|
||||
|
||||
{profilePicture && <Avatar src={profilePicture} />}
|
||||
|
||||
<Input
|
||||
label="Banner Picture"
|
||||
labelPlacement="outside"
|
||||
name="bannerPicture"
|
||||
placeholder="Enter a url to an image"
|
||||
type="text"
|
||||
value={bannerPicture}
|
||||
onValueChange={setBannerPicture}
|
||||
/>
|
||||
|
||||
{bannerPicture &&
|
||||
bannerPicture.startsWith("https://") &&
|
||||
bannerPicture.length > 8 && (
|
||||
<div className="bg-[#222222] h-28 w-full relative">
|
||||
<Image
|
||||
src={bannerPicture}
|
||||
alt={`${user.name}'s profile banner`}
|
||||
className="object-cover"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
{waitingSave ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>Save</p>
|
||||
)}
|
||||
</Button>
|
||||
<Button type="reset" variant="flat">
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Form, Input } from "@nextui-org/react";
|
||||
import { Button, Form, Input, Link } from "@nextui-org/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
@ -74,6 +74,7 @@ export default function UserPage() {
|
|||
body: JSON.stringify({ username: username, password: password }),
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -131,6 +132,9 @@ export default function UserPage() {
|
|||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[#333] dark:text-white transition-color duration-250">
|
||||
Already have an account? <Link href="/login">Log In</Link>
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
17
src/app/theme-slaughter/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import ThemeSlaughter from "@/components/themes/theme-slaughter";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="flex justify-between flex-wrap">
|
||||
<div className="md:w-2/3">
|
||||
<ThemeSlaughter />
|
||||
</div>
|
||||
<div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
17
src/app/theme-suggestions/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import ThemeSuggestions from "@/components/themes/theme-suggest";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="flex justify-between flex-wrap">
|
||||
<div className="md:w-2/3">
|
||||
<ThemeSuggestions />
|
||||
</div>
|
||||
<div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
17
src/app/theme-voting/page.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Timers from "@/components/timers";
|
||||
import Streams from "@/components/streams";
|
||||
import ThemeVoting from "@/components/themes/theme-vote";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="flex justify-between flex-wrap">
|
||||
<div className="md:w-2/3">
|
||||
<ThemeVoting />
|
||||
</div>
|
||||
<div>
|
||||
<Timers />
|
||||
<Streams />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +1,59 @@
|
|||
import User from "../../../components/user";
|
||||
"use client";
|
||||
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { Avatar } from "@nextui-org/react";
|
||||
import Image from "next/image";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function UserPage() {
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const { slug } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/user?slug=${slug}`
|
||||
: `http://localhost:3005/api/v1/user?slug=${slug}`
|
||||
);
|
||||
setUser(await response.json());
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, [slug]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<User />
|
||||
{user && (
|
||||
<div className="border-2 border-[#222224] relative rounded-xl overflow-hidden bg-[#18181a]">
|
||||
<div className="bg-[#222222] h-28 relative">
|
||||
{user.bannerPicture && (
|
||||
<Image
|
||||
src={user.bannerPicture}
|
||||
alt={`${user.name}'s profile banner`}
|
||||
className="object-cover"
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Avatar
|
||||
className="absolute rounded-full left-16 top-16 h-24 w-24 bg-transparent"
|
||||
src={user.profilePicture}
|
||||
/>
|
||||
<div className="p-8 mt-8">
|
||||
<p className="text-3xl">{user.name}</p>
|
||||
<div
|
||||
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
user.bio && user.bio != "<p></p>" ? user.bio : "No user bio",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return <div></div>;
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
"use client";
|
||||
|
||||
export default function Announcements() {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
144
src/components/editor/EditorMenuBar.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
"use client";
|
||||
|
||||
import { Editor } from "@tiptap/react";
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
Code,
|
||||
Highlighter,
|
||||
Italic,
|
||||
Minus,
|
||||
Quote,
|
||||
Redo,
|
||||
Strikethrough,
|
||||
Subscript,
|
||||
Superscript,
|
||||
Underline,
|
||||
Undo,
|
||||
} from "lucide-react";
|
||||
import EditorMenuButton from "./EditorMenuButton";
|
||||
|
||||
type EditorMenuProps = {
|
||||
editor: Editor | null;
|
||||
};
|
||||
|
||||
export default function EditorMenuBar({ editor }: EditorMenuProps) {
|
||||
if (!editor) return null;
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
icon: <Bold size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleBold().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("bold"),
|
||||
},
|
||||
{
|
||||
icon: <Italic size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleItalic().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("italic"),
|
||||
},
|
||||
{
|
||||
icon: <Underline size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleUnderline().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("underline"),
|
||||
},
|
||||
{
|
||||
icon: <Highlighter size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleHighlight().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("highlight"),
|
||||
},
|
||||
{
|
||||
icon: <Strikethrough size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleStrike().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("strike"),
|
||||
},
|
||||
{
|
||||
icon: <Subscript size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleSubscript().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("subscript"),
|
||||
},
|
||||
{
|
||||
icon: <Superscript size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleSuperscript().run(),
|
||||
disabled: false,
|
||||
isActive: editor.isActive("superscript"),
|
||||
},
|
||||
{
|
||||
icon: <Minus size={20} />,
|
||||
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
||||
disabled: !editor.can().setHorizontalRule(),
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
icon: <Quote size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
||||
disabled: !editor.can().toggleBlockquote(),
|
||||
isActive: editor.isActive("blockquote"),
|
||||
},
|
||||
{
|
||||
icon: <Code size={20} />,
|
||||
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
disabled: !editor.can().toggleCodeBlock(),
|
||||
isActive: editor.isActive("codeblock"),
|
||||
},
|
||||
{
|
||||
icon: <AlignLeft size={20} />,
|
||||
onClick: () => editor.chain().focus().setTextAlign("left").run(),
|
||||
disabled: !editor.can().setTextAlign("left"),
|
||||
isActive: editor.isActive("textalign"),
|
||||
},
|
||||
{
|
||||
icon: <AlignRight size={20} />,
|
||||
onClick: () => editor.chain().focus().setTextAlign("right").run(),
|
||||
disabled: !editor.can().setTextAlign("right"),
|
||||
isActive: editor.isActive("textalign"),
|
||||
},
|
||||
{
|
||||
icon: <AlignCenter size={20} />,
|
||||
onClick: () => editor.chain().focus().setTextAlign("center").run(),
|
||||
disabled: !editor.can().setTextAlign("center"),
|
||||
isActive: editor.isActive("textalign"),
|
||||
},
|
||||
{
|
||||
icon: <AlignJustify size={20} />,
|
||||
onClick: () => editor.chain().focus().setTextAlign("justify").run(),
|
||||
disabled: !editor.can().setTextAlign("justify"),
|
||||
isActive: editor.isActive("textalign"),
|
||||
},
|
||||
{
|
||||
icon: <Undo size={20} />,
|
||||
onClick: () => editor.chain().focus().undo().run(),
|
||||
disabled: !editor.can().undo(),
|
||||
isActive: editor.isActive("undo"),
|
||||
},
|
||||
{
|
||||
icon: <Redo size={20} />,
|
||||
onClick: () => editor.chain().focus().redo().run(),
|
||||
disabled: !editor.can().redo(),
|
||||
isActive: editor.isActive("redo"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex space-x-2">
|
||||
{buttons.map(({ icon, onClick, disabled, isActive }, index) => (
|
||||
<EditorMenuButton
|
||||
key={index}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon}
|
||||
</EditorMenuButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
32
src/components/editor/EditorMenuButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
|
||||
type EditorMenuButtonProps = {
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function EditorMenuButton({
|
||||
onClick,
|
||||
isActive,
|
||||
disabled,
|
||||
children,
|
||||
}: EditorMenuButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={onClick}
|
||||
isDisabled={disabled}
|
||||
size="sm"
|
||||
isIconOnly
|
||||
className={`${
|
||||
isActive ? "bg-blue-500 data-[hover=true]:bg-blue-400" : ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
164
src/components/editor/index.tsx
Normal file
|
@ -0,0 +1,164 @@
|
|||
"use client";
|
||||
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import Document from "@tiptap/extension-document";
|
||||
import Paragraph from "@tiptap/extension-paragraph";
|
||||
import Text from "@tiptap/extension-text";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import EditorMenuBar from "./EditorMenuBar";
|
||||
import Bold from "@tiptap/extension-bold";
|
||||
import Italic from "@tiptap/extension-italic";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import Strike from "@tiptap/extension-strike";
|
||||
import Subscript from "@tiptap/extension-subscript";
|
||||
import Superscript from "@tiptap/extension-superscript";
|
||||
import History from "@tiptap/extension-history";
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import Blockquote from "@tiptap/extension-blockquote";
|
||||
import Heading from "@tiptap/extension-heading";
|
||||
import ListItem from "@tiptap/extension-list-item";
|
||||
import OrderedList from "@tiptap/extension-ordered-list";
|
||||
import BulletList from "@tiptap/extension-bullet-list";
|
||||
import HardBreak from "@tiptap/extension-hard-break";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import Typography from "@tiptap/extension-typography";
|
||||
import Dropcursor from "@tiptap/extension-dropcursor";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import Table from "@tiptap/extension-table";
|
||||
import TableCell from "@tiptap/extension-table-cell";
|
||||
import TableHeader from "@tiptap/extension-table-header";
|
||||
import TableRow from "@tiptap/extension-table-row";
|
||||
import Youtube from "@tiptap/extension-youtube";
|
||||
import CodeBlock from "@tiptap/extension-code-block";
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
type EditorProps = {
|
||||
content: string;
|
||||
setContent: (content: string) => void;
|
||||
gameEditor?: boolean;
|
||||
};
|
||||
|
||||
const limit = 32767;
|
||||
|
||||
export default function Editor({ content, setContent,gameEditor }: EditorProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CharacterCount.configure({
|
||||
limit,
|
||||
}),
|
||||
Bold,
|
||||
Italic,
|
||||
Underline,
|
||||
Highlight,
|
||||
Strike,
|
||||
Subscript,
|
||||
Superscript,
|
||||
History,
|
||||
HorizontalRule,
|
||||
Blockquote,
|
||||
Heading,
|
||||
ListItem,
|
||||
OrderedList,
|
||||
BulletList,
|
||||
HardBreak,
|
||||
Markdown.configure({
|
||||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
Typography,
|
||||
TextAlign.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
}),
|
||||
Dropcursor,
|
||||
Image,
|
||||
TaskItem,
|
||||
TaskList,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
Youtube,
|
||||
CodeBlock,
|
||||
],
|
||||
content: content,
|
||||
immediatelyRender: false,
|
||||
onUpdate: ({ editor }) => {
|
||||
setContent(editor.getHTML());
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose dark:prose-invert " +
|
||||
(gameEditor
|
||||
? "min-h-[600px] max-h-[600px]"
|
||||
: "min-h-[150px] max-h-[400px]") +
|
||||
" overflow-y-auto cursor-text rounded-md border p-5 focus-within:outline-none focus-within:border-blue-500 !duration-250 !ease-linear !transition-all",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<EditorMenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
<Spacer y={3} />
|
||||
{editor && (
|
||||
<div
|
||||
className={`${
|
||||
editor.storage.characterCount.characters() === limit
|
||||
? "text-red-500"
|
||||
: editor.storage.characterCount.characters() > limit / 2
|
||||
? "text-yellow-500"
|
||||
: "text-[#888] dark:text-[#555]"
|
||||
} transform-color duration-250 ease-linear flex items-center gap-3`}
|
||||
>
|
||||
<svg width="30" height="30" viewBox="0 0 36 36">
|
||||
<circle
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="15.915"
|
||||
fill="none"
|
||||
stroke={theme === "dark" ? "#333" : "#eee"}
|
||||
strokeWidth="3"
|
||||
className="!duration-250 !ease-linear !transition-all"
|
||||
/>
|
||||
<circle
|
||||
id="progress-circle"
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="15.915"
|
||||
fill="none"
|
||||
stroke={
|
||||
editor.storage.characterCount.characters() === limit
|
||||
? "#de362a"
|
||||
: editor.storage.characterCount.characters() > limit / 2
|
||||
? "#eab308"
|
||||
: "#26d1ff"
|
||||
}
|
||||
strokeWidth="3"
|
||||
strokeDasharray="100, 100"
|
||||
strokeDashoffset={
|
||||
(1 - editor.storage.characterCount.characters() / limit) * 100
|
||||
}
|
||||
transform="rotate(-90 18 18)"
|
||||
className="!duration-250 !ease-linear !transition-all"
|
||||
/>
|
||||
</svg>
|
||||
{editor.storage.characterCount.characters()} / {limit} characters
|
||||
<br />
|
||||
{editor.storage.characterCount.words()} words
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
20
src/components/footer/index.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import IconLink from "../link-components/IconLink";
|
||||
import { SiDiscord, SiForgejo, SiGithub } from "@icons-pack/react-simple-icons";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="p-8 bg-[#fff] dark:bg-[#222] mt-8 border-t-2 dark:border-white/15 border-black/15 transition-color duration-500 ease-in-out">
|
||||
<div className="flex justify-between">
|
||||
<div></div>
|
||||
<div className="flex gap-3">
|
||||
<IconLink icon={<SiGithub />} href="https://github.com/Dare2Jam" />
|
||||
<IconLink
|
||||
icon={<SiForgejo />}
|
||||
href="https://git.edikoyo.com/Ategon/Jamjar"
|
||||
/>
|
||||
<IconLink icon={<SiDiscord />} href="https://discord.d2jam.com" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
170
src/components/jam-header/index.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
"use client";
|
||||
|
||||
import { Calendar } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCurrentJam, ActiveJamResponse } from "../../helpers/jam";
|
||||
|
||||
export default function JamHeader() {
|
||||
const [activeJamResponse, setActiveJamResponse] =
|
||||
useState<ActiveJamResponse | null>(null);
|
||||
const [topTheme, setTopTheme] = useState<string | null>(null);
|
||||
|
||||
// Fetch active jam details
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const jamData = await getCurrentJam();
|
||||
setActiveJamResponse(jamData);
|
||||
console.log(jamData);
|
||||
// If we're in Jamming phase, fetch top themes and pick the first one
|
||||
if ((jamData?.phase === "Jamming" || jamData?.phase === "Rating") && jamData.jam) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/themes/top-themes"
|
||||
: "http://localhost:3005/api/v1/themes/top-themes"
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const themes = await response.json();
|
||||
if (themes.length > 0) {
|
||||
setTopTheme(themes[0].suggestion);
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to fetch top themes.", response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching top themes:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Helper function to get ordinal suffix
|
||||
const getOrdinalSuffix = (day: number): string => {
|
||||
if (day > 3 && day < 21) return "th";
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#7090b9] dark:bg-[#124a88] flex flex-col rounded-2xl overflow-hidden text-white transition-color duration-250">
|
||||
{/* Jam Header */}
|
||||
<div className="flex">
|
||||
<div className="bg-[#85bdd2] dark:bg-[#1892b3] p-4 px-6 flex items-center gap-2 font-bold transition-color duration-250">
|
||||
<Calendar />
|
||||
<p>
|
||||
{activeJamResponse?.jam && activeJamResponse.phase ? (
|
||||
<span className="text-sm font-normal">
|
||||
{activeJamResponse.jam.name} - {activeJamResponse.phase} Phase
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm font-normal">(No Active Jams)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 px-6 font-bold">
|
||||
<p>
|
||||
{activeJamResponse?.jam ? (
|
||||
<>
|
||||
{new Date(activeJamResponse.jam.startTime).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
month: "long",
|
||||
}
|
||||
)}{" "}
|
||||
{new Date(activeJamResponse.jam.startTime).getDate()}
|
||||
{getOrdinalSuffix(
|
||||
new Date(activeJamResponse.jam.startTime).getDate()
|
||||
)}
|
||||
{" - "}
|
||||
{new Date(
|
||||
new Date(activeJamResponse.jam.startTime).getTime() +
|
||||
activeJamResponse.jam.jammingHours * 60 * 60 * 1000
|
||||
).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
})}{" "}
|
||||
{new Date(
|
||||
new Date(activeJamResponse.jam.startTime).getTime() +
|
||||
activeJamResponse.jam.jammingHours * 60 * 60 * 1000
|
||||
).getDate()}
|
||||
{getOrdinalSuffix(
|
||||
new Date(
|
||||
new Date(activeJamResponse.jam.startTime).getTime() +
|
||||
activeJamResponse.jam.jammingHours * 60 * 60 * 1000
|
||||
).getDate()
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"Dates TBA"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeJamResponse?.phase === "Suggestion" && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
|
||||
<a
|
||||
href="/theme-suggestions"
|
||||
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
|
||||
>
|
||||
Go to Theme Suggestion
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeJamResponse?.phase === "Survival" && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
|
||||
<a
|
||||
href="/theme-slaughter"
|
||||
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
|
||||
>
|
||||
Go to Theme Survival
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeJamResponse?.phase === "Voting" && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
|
||||
<a
|
||||
href="/theme-voting"
|
||||
className="text-blue-300 dark:text-blue-500 hover:underline font-semibold"
|
||||
>
|
||||
Go to Theme Voting
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeJamResponse?.phase === "Jamming" && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
|
||||
{topTheme ? (
|
||||
<p className="text-xl font-bold text-blue-500">THEME: {topTheme}</p>
|
||||
) : (
|
||||
<p>No top-scoring theme available.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeJamResponse?.phase === "Rating" && (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-4 text-center rounded-b-2x">
|
||||
{topTheme ? (
|
||||
<p className="text-xl font-bold text-blue-500">
|
||||
THEME: {topTheme} RESULTS
|
||||
</p>
|
||||
) : (
|
||||
<p>No top-scoring theme available.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
45
src/components/link-components/ButtonAction.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
interface ButtonActionProps {
|
||||
icon?: ReactNode;
|
||||
onPress: () => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function ButtonAction({
|
||||
icon,
|
||||
onPress,
|
||||
name,
|
||||
}: ButtonActionProps) {
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button
|
||||
endContent={icon}
|
||||
className={`text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform duration-500 ease-in-out ${
|
||||
!reduceMotion ? "hover:scale-110" : ""
|
||||
}`}
|
||||
variant="bordered"
|
||||
onPress={onPress}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
);
|
||||
}
|
47
src/components/link-components/ButtonLink.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Link } from "@nextui-org/react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
interface ButtonLinkProps {
|
||||
icon?: ReactNode;
|
||||
href: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function ButtonLink({ icon, href, name }: ButtonLinkProps) {
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex justify-center duration-500 ease-in-out transition-all transform ${
|
||||
!reduceMotion ? "hover:scale-110" : ""
|
||||
}`}
|
||||
>
|
||||
<Button
|
||||
endContent={icon}
|
||||
className={`text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out ${
|
||||
!reduceMotion ? "hover:scale-110" : ""
|
||||
}`}
|
||||
variant="bordered"
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
38
src/components/link-components/IconLink.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
"use client";
|
||||
|
||||
import { Link } from "@nextui-org/react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
interface IconLinkProps {
|
||||
icon: ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function IconLink({ icon, href }: IconLinkProps) {
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
|
||||
!reduceMotion ? "hover:scale-125" : ""
|
||||
} transition-color`}
|
||||
>
|
||||
{icon}
|
||||
</Link>
|
||||
);
|
||||
}
|
38
src/components/link-components/Link.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
"use client";
|
||||
|
||||
import { Link as BaseLink } from "@nextui-org/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface LinkProps {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function Link({ name, href }: LinkProps) {
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BaseLink
|
||||
href={href}
|
||||
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
|
||||
!reduceMotion ? "hover:scale-110" : ""
|
||||
} transition-color`}
|
||||
>
|
||||
{name}
|
||||
</BaseLink>
|
||||
);
|
||||
}
|
115
src/components/navbar/MobileNavbar.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Image,
|
||||
Link,
|
||||
NavbarBrand,
|
||||
Navbar as NavbarBase,
|
||||
NavbarContent,
|
||||
} from "@nextui-org/react";
|
||||
import NavbarButtonLink from "./NavbarButtonLink";
|
||||
import { LogInIcon, NotebookPen } from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { getCurrentJam } from "@/helpers/jam";
|
||||
import { JamType } from "@/types/JamType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import MobileNavbarUser from "./MobileNavbarUser";
|
||||
import ThemeToggle from "../theme-toggle";
|
||||
|
||||
|
||||
export default function MobileNavbar() {
|
||||
const pathname = usePathname();
|
||||
const [jam, setJam] = useState<JamType | null>();
|
||||
const [isInJam, setIsInJam] = useState<boolean>();
|
||||
|
||||
const [user, setUser] = useState<UserType>();
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
async function loadUser() {
|
||||
const jamResponse = await getCurrentJam();
|
||||
const currentJam = jamResponse?.jam;
|
||||
setJam(currentJam);
|
||||
|
||||
if (!hasCookie("token")) {
|
||||
setUser(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
|
||||
if (
|
||||
currentJam &&
|
||||
user.jams.filter((jam: JamType) => jam.id == currentJam.id).length > 0
|
||||
) {
|
||||
setIsInJam(true);
|
||||
} else {
|
||||
setIsInJam(false);
|
||||
}
|
||||
|
||||
if (response.status == 200) {
|
||||
setUser(user);
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<NavbarBase maxWidth="2xl" className="bg-[#222] p-1" isBordered height={80}>
|
||||
<NavbarContent justify="start" className="gap-10">
|
||||
<NavbarBrand className="flex-grow-0">
|
||||
<Link
|
||||
href="/"
|
||||
className="duration-500 ease-in-out transition-all transform hover:scale-110"
|
||||
>
|
||||
<Image
|
||||
as={NextImage}
|
||||
src="/images/D2J_Icon.png"
|
||||
className="min-w-[70px]"
|
||||
alt="Down2Jam logo"
|
||||
width={70}
|
||||
height={59.7}
|
||||
/>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent justify="end" className="gap-4">
|
||||
<ThemeToggle />
|
||||
{!user && (
|
||||
<NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" />
|
||||
)}
|
||||
{!user && (
|
||||
<NavbarButtonLink
|
||||
icon={<NotebookPen />}
|
||||
name="Sign Up"
|
||||
href="/signup"
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<MobileNavbarUser
|
||||
user={user}
|
||||
isInJam={isInJam}
|
||||
setIsInJam={setIsInJam}
|
||||
jam={jam}
|
||||
/>
|
||||
)}
|
||||
</NavbarContent>
|
||||
</NavbarBase>
|
||||
);
|
||||
}
|
123
src/components/navbar/MobileNavbarUser.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import { JamType } from "@/types/JamType";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { getCurrentJam, joinJam } from "@/helpers/jam";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
interface NavbarUserProps {
|
||||
user: UserType;
|
||||
jam?: JamType | null;
|
||||
setIsInJam: Dispatch<SetStateAction<boolean | undefined>>;
|
||||
isInJam?: boolean;
|
||||
}
|
||||
|
||||
export default function MobileNavbarUser({
|
||||
user,
|
||||
jam,
|
||||
setIsInJam,
|
||||
isInJam,
|
||||
}: NavbarUserProps) {
|
||||
const [currentJam, setCurrentJam] = useState<JamType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentJam = async () => {
|
||||
try {
|
||||
const response = await getCurrentJam();
|
||||
setCurrentJam(response?.jam || null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching current jam:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCurrentJam();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NavbarItem>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
src={user.profilePicture}
|
||||
className="cursor-pointer"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
{jam && currentJam && isInJam ? (
|
||||
<DropdownItem
|
||||
key="create-game"
|
||||
href="/create-game"
|
||||
className="text-black"
|
||||
>
|
||||
Create Game
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
{jam && currentJam && !isInJam ? (
|
||||
<DropdownItem
|
||||
key="join-event"
|
||||
className="text-black"
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (!currentJam) {
|
||||
toast.error("There is no jam to join");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await joinJam(currentJam.id)) {
|
||||
setIsInJam(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during join process:", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Join Event
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
<DropdownItem
|
||||
key="create-post"
|
||||
href="/create-post"
|
||||
className="text-black"
|
||||
>
|
||||
Create Post
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
className="text-black"
|
||||
href={`/u/${user.slug}`}
|
||||
>
|
||||
Profile
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
showDivider
|
||||
key="settings"
|
||||
className="text-black"
|
||||
href="/settings"
|
||||
>
|
||||
Settings
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
color="danger"
|
||||
className="text-danger"
|
||||
href="/logout"
|
||||
>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
21
src/components/navbar/NavbarButtonAction.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { NavbarItem } from "@nextui-org/react";
|
||||
import { ReactNode } from "react";
|
||||
import ButtonAction from "../link-components/ButtonAction";
|
||||
|
||||
interface NavbarButtonActionProps {
|
||||
icon?: ReactNode;
|
||||
onPress: () => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function NavbarButtonAction({
|
||||
icon,
|
||||
onPress,
|
||||
name,
|
||||
}: NavbarButtonActionProps) {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<ButtonAction icon={icon} onPress={onPress} name={name} />
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
21
src/components/navbar/NavbarButtonLink.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { NavbarItem } from "@nextui-org/react";
|
||||
import { ReactNode } from "react";
|
||||
import ButtonLink from "../link-components/ButtonLink";
|
||||
|
||||
interface NavbarButtonLinkProps {
|
||||
icon?: ReactNode;
|
||||
href: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function NavbarButtonLink({
|
||||
icon,
|
||||
href,
|
||||
name,
|
||||
}: NavbarButtonLinkProps) {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<ButtonLink icon={icon} href={href} name={name} />
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
16
src/components/navbar/NavbarIconLink.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { NavbarItem } from "@nextui-org/react";
|
||||
import { ReactNode } from "react";
|
||||
import IconLink from "../link-components/IconLink";
|
||||
|
||||
interface NavbarIconLinkProps {
|
||||
icon: ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function NavbarIconLink({ icon, href }: NavbarIconLinkProps) {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<IconLink icon={icon} href={href} />
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
15
src/components/navbar/NavbarLink.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { NavbarItem } from "@nextui-org/react";
|
||||
import Link from "../link-components/Link";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function NavbarLink({ name, href }: NavbarLinkProps) {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<Link name={name} href={href} />
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
14
src/components/navbar/NavbarSearchbar.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Input, NavbarItem } from "@nextui-org/react";
|
||||
|
||||
export default function NavbarSearchbar() {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
classNames={{
|
||||
inputWrapper: "!duration-500 ease-in-out transition-all",
|
||||
}}
|
||||
/>
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
220
src/components/navbar/PCNavbar.tsx
Normal file
|
@ -0,0 +1,220 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Image,
|
||||
Link,
|
||||
NavbarBrand,
|
||||
Navbar as NavbarBase,
|
||||
NavbarContent,
|
||||
Divider,
|
||||
} from "@nextui-org/react";
|
||||
import NavbarLink from "./NavbarLink";
|
||||
import NavbarSearchbar from "./NavbarSearchbar";
|
||||
import NavbarButtonLink from "./NavbarButtonLink";
|
||||
import {
|
||||
Bell,
|
||||
CalendarPlus,
|
||||
Gamepad2,
|
||||
LogInIcon,
|
||||
NotebookPen,
|
||||
Shield,
|
||||
SquarePen,
|
||||
} from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { getCurrentJam, joinJam } from "@/helpers/jam";
|
||||
import { JamType } from "@/types/JamType";
|
||||
import { GameType } from "@/types/GameType";
|
||||
import { UserType } from "@/types/UserType";
|
||||
import NavbarUser from "./PCNavbarUser";
|
||||
import NavbarButtonAction from "./NavbarButtonAction";
|
||||
import { toast } from "react-toastify";
|
||||
import NavbarIconLink from "./NavbarIconLink";
|
||||
import ThemeToggle from "../theme-toggle";
|
||||
|
||||
export default function PCNavbar() {
|
||||
const pathname = usePathname();
|
||||
const [jam, setJam] = useState<JamType | null>();
|
||||
const [isInJam, setIsInJam] = useState<boolean>();
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
|
||||
const [hasGame, setHasGame] = useState<GameType | null>();
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
setReduceMotion(mediaQuery.matches);
|
||||
|
||||
const handleChange = (event: MediaQueryListEvent) => {
|
||||
setReduceMotion(event.matches);
|
||||
};
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
async function loadUser() {
|
||||
const jamResponse = await getCurrentJam();
|
||||
const currentJam = jamResponse?.jam;
|
||||
setJam(currentJam);
|
||||
|
||||
if (!hasCookie("token")) {
|
||||
setUser(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
const user = await response.json();
|
||||
|
||||
// Check if user has a game in current jam
|
||||
const gameResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/self/current-game?username=${getCookie("user")}`
|
||||
: `http://localhost:3005/api/v1/self/current-game?username=${getCookie("user")}`,
|
||||
{
|
||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (gameResponse.ok) {
|
||||
const gameData = await gameResponse.json();
|
||||
console.log("Game Data:", gameData); // Log game data
|
||||
console.log("User Data:", user); // Log user data
|
||||
|
||||
if (gameData) {
|
||||
// Check if the logged-in user is either the creator or a contributor
|
||||
const isContributor =
|
||||
gameData.author?.id === user.id || // Check if logged-in user is the author
|
||||
gameData.contributors?.some((contributor: UserType) => contributor.id === user.id); // Check if logged-in user is a contributor
|
||||
|
||||
console.log("Is Contributor:", isContributor); // Log whether the user is a contributor
|
||||
|
||||
if (isContributor) {
|
||||
setHasGame(gameData); // Set the game data for "My Game"
|
||||
} else {
|
||||
setHasGame(null); // No game associated with this user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentJam &&
|
||||
user.jams.filter((jam: JamType) => jam.id == currentJam.id).length > 0
|
||||
) {
|
||||
setIsInJam(true);
|
||||
} else {
|
||||
setIsInJam(false);
|
||||
}
|
||||
|
||||
if (response.status == 200) {
|
||||
setUser(user);
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
|
||||
return (
|
||||
<NavbarBase
|
||||
maxWidth="2xl"
|
||||
className="bg-[#fff] dark:bg-[#222] p-1 duration-500 ease-in-out transition-color"
|
||||
isBordered
|
||||
height={80}
|
||||
>
|
||||
{/* Left side navbar items */}
|
||||
<NavbarContent justify="start" className="gap-10">
|
||||
<NavbarBrand className="flex-grow-0">
|
||||
<Link
|
||||
href="/"
|
||||
className={`duration-500 ease-in-out transition-all transform ${
|
||||
reduceMotion ? "" : "hover:scale-110"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
as={NextImage}
|
||||
src="/images/D2J_Icon.png"
|
||||
className="min-w-[70px]"
|
||||
alt="Down2Jam logo"
|
||||
width={70}
|
||||
height={70}
|
||||
/>
|
||||
</Link>
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarLink href="/about" name="About" />
|
||||
<NavbarLink href="/games" name="Games" />
|
||||
</NavbarContent>
|
||||
|
||||
|
||||
<NavbarContent justify="end" className="gap-4">
|
||||
<NavbarSearchbar />
|
||||
{user && <Divider orientation="vertical" className="h-1/2" />}
|
||||
{user && jam && isInJam && (
|
||||
<NavbarButtonLink
|
||||
icon={<Gamepad2 />}
|
||||
name={hasGame ? "My Game" : "Create Game"}
|
||||
href={hasGame ? "/games/"+hasGame.slug : "/create-game"}
|
||||
/>
|
||||
)}
|
||||
{user && jam && !isInJam && (
|
||||
<NavbarButtonAction
|
||||
icon={<CalendarPlus />}
|
||||
name="Join jam"
|
||||
onPress={async () => {
|
||||
const currentJamResponse = await getCurrentJam();
|
||||
const currentJam = currentJamResponse?.jam;
|
||||
|
||||
if (!currentJam) {
|
||||
toast.error("There is no jam to join");
|
||||
return;
|
||||
}
|
||||
if (await joinJam(currentJam.id)) {
|
||||
setIsInJam(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<NavbarButtonLink
|
||||
icon={<SquarePen />}
|
||||
name="Create Post"
|
||||
href="/create-post"
|
||||
/>
|
||||
)}
|
||||
{user && <NavbarIconLink icon={<Bell />} href="/inbox" />}
|
||||
{user && user.mod && (
|
||||
<NavbarIconLink icon={<Shield />} href="/reports" />
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<Divider orientation="vertical" className="h-1/2" />
|
||||
{!user && (
|
||||
<NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" />
|
||||
)}
|
||||
{!user && (
|
||||
<NavbarButtonLink
|
||||
icon={<NotebookPen />}
|
||||
name="Sign Up"
|
||||
href="/signup"
|
||||
/>
|
||||
)}
|
||||
{user && <NavbarUser user={user} />}
|
||||
</NavbarContent>
|
||||
</NavbarBase>
|
||||
);
|
||||
}
|
59
src/components/navbar/PCNavbarUser.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
Avatar,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownSection,
|
||||
DropdownTrigger,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import { UserType } from "@/types/UserType";
|
||||
|
||||
interface NavbarUserProps {
|
||||
user: UserType;
|
||||
}
|
||||
|
||||
export default function PCNavbarUser({ user }: NavbarUserProps) {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<Dropdown backdrop="opaque">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
src={user.profilePicture}
|
||||
className="cursor-pointer"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownSection title={user.name}>
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
className="text-[#333] dark:text-white"
|
||||
href={`/u/${user.slug}`}
|
||||
>
|
||||
Profile
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
showDivider
|
||||
key="settings"
|
||||
className="text-[#333] dark:text-white"
|
||||
href="/settings"
|
||||
>
|
||||
Settings
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
color="danger"
|
||||
className="text-danger"
|
||||
href="/logout"
|
||||
>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|