Compare commits
63 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 |
|
@ -1,7 +1,14 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
4138
package-lock.json
generated
45
package.json
|
@ -11,18 +11,61 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@icons-pack/react-simple-icons": "^11.0.1",
|
"@icons-pack/react-simple-icons": "^11.0.1",
|
||||||
"@nextui-org/react": "^2.4.8",
|
"@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",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^11.11.9",
|
"framer-motion": "^11.11.9",
|
||||||
|
"i": "^0.3.7",
|
||||||
|
"install": "^0.13.0",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"next": "15.0.1",
|
"next": "15.0.1",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
|
"npm": "^11.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.0.1",
|
"eslint-config-next": "15.0.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
|
BIN
public/images/D2J_Icon.png
Normal file
After ![]() (image error) Size: 24 KiB |
Before ![]() (image error) Size: 1.1 MiB |
Before ![]() (image error) Size: 25 KiB |
BIN
public/images/tags/art.png
Normal file
After ![]() (image error) Size: 22 KiB |
BIN
public/images/tags/aseprite.png
Normal file
After ![]() (image error) Size: 6.1 KiB |
BIN
public/images/tags/bevy.png
Normal file
After ![]() (image error) Size: 9.2 KiB |
BIN
public/images/tags/bitsy.png
Normal file
After ![]() (image error) Size: 1.5 KiB |
BIN
public/images/tags/blender.webp
Normal file
After ![]() (image error) Size: 3.4 KiB |
BIN
public/images/tags/clickteam.png
Normal file
After ![]() (image error) Size: 7.6 KiB |
BIN
public/images/tags/console-controller.png
Normal file
After ![]() (image error) Size: 18 KiB |
BIN
public/images/tags/construct.png
Normal file
After ![]() (image error) Size: 96 KiB |
BIN
public/images/tags/cryengine.png
Normal file
After ![]() (image error) Size: 2.3 KiB |
BIN
public/images/tags/d2jam.png
Normal file
After ![]() (image error) Size: 24 KiB |
BIN
public/images/tags/defold.png
Normal file
After ![]() (image error) Size: 6.1 KiB |
BIN
public/images/tags/devlog.png
Normal file
After ![]() (image error) Size: 43 KiB |
BIN
public/images/tags/digitalart.png
Normal file
After ![]() (image error) Size: 27 KiB |
BIN
public/images/tags/flickgame.png
Normal file
After ![]() (image error) Size: 118 B |
BIN
public/images/tags/gameart.png
Normal file
After ![]() (image error) Size: 11 KiB |
BIN
public/images/tags/gameassets.png
Normal file
After ![]() (image error) Size: 40 KiB |
BIN
public/images/tags/gamedesign.png
Normal file
After ![]() (image error) Size: 42 KiB |
BIN
public/images/tags/gamejam.png
Normal file
After ![]() (image error) Size: 45 KiB |
BIN
public/images/tags/gamemaker.png
Normal file
After ![]() (image error) Size: 8.6 KiB |
BIN
public/images/tags/gbstudio.png
Normal file
After ![]() (image error) Size: 70 KiB |
BIN
public/images/tags/gdevelop.png
Normal file
After ![]() (image error) Size: 119 KiB |
BIN
public/images/tags/glsl.png
Normal file
After ![]() (image error) 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 (image error) Size: 3.5 KiB |
BIN
public/images/tags/haxe.png
Normal file
After ![]() (image error) Size: 2.8 KiB |
BIN
public/images/tags/indiedev.png
Normal file
After ![]() (image error) Size: 54 KiB |
BIN
public/images/tags/itch.png
Normal file
After ![]() (image error) Size: 111 KiB |
BIN
public/images/tags/libgdx.png
Normal file
After ![]() (image error) Size: 8.9 KiB |
BIN
public/images/tags/love.png
Normal file
After ![]() (image error) Size: 6 KiB |
BIN
public/images/tags/lowpoly.png
Normal file
After ![]() (image error) Size: 43 KiB |
BIN
public/images/tags/meme.png
Normal file
After ![]() (image error) Size: 61 KiB |
BIN
public/images/tags/music.png
Normal file
After ![]() (image error) Size: 37 KiB |
BIN
public/images/tags/newspaper.png
Normal file
After ![]() (image error) Size: 24 KiB |
BIN
public/images/tags/notebook.png
Normal file
After ![]() (image error) Size: 27 KiB |
BIN
public/images/tags/pico8.png
Normal file
After ![]() (image error) Size: 2.2 KiB |
BIN
public/images/tags/pixelart.png
Normal file
After ![]() (image error) Size: 29 KiB |
BIN
public/images/tags/postmortem.png
Normal file
After ![]() (image error) Size: 43 KiB |
BIN
public/images/tags/puzzlescript.png
Normal file
After ![]() (image error) Size: 3.1 KiB |
BIN
public/images/tags/pygame.png
Normal file
After ![]() (image error) Size: 48 KiB |
BIN
public/images/tags/renpy.png
Normal file
After ![]() (image error) Size: 60 KiB |
BIN
public/images/tags/rpgmaker.png
Normal file
After ![]() (image error) Size: 357 KiB |
BIN
public/images/tags/rust.png
Normal file
After ![]() (image error) Size: 84 KiB |
BIN
public/images/tags/saturday.png
Normal file
After ![]() (image error) Size: 50 KiB |
BIN
public/images/tags/scratch.png
Normal file
After ![]() (image error) Size: 15 KiB |
BIN
public/images/tags/shaders.png
Normal file
After ![]() (image error) Size: 51 KiB |
BIN
public/images/tags/steam.png
Normal file
After ![]() (image error) Size: 137 KiB |
BIN
public/images/tags/stencyl.png
Normal file
After ![]() (image error) Size: 43 KiB |
BIN
public/images/tags/stream.png
Normal file
After ![]() (image error) Size: 3.9 KiB |
BIN
public/images/tags/teamup.png
Normal file
After ![]() (image error) Size: 61 KiB |
BIN
public/images/tags/tic80.png
Normal file
After ![]() (image error) Size: 4.2 KiB |
BIN
public/images/tags/twine.png
Normal file
After ![]() (image error) Size: 19 KiB |
BIN
public/images/tags/tyrano.png
Normal file
After ![]() (image error) Size: 17 KiB |
BIN
public/images/tags/unity.png
Normal file
After ![]() (image error) Size: 4.1 KiB |
BIN
public/images/tags/unreal.png
Normal file
After ![]() (image error) 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";
|
"use client";
|
||||||
|
|
||||||
|
import Editor from "@/components/editor";
|
||||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
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 { redirect } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
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() {
|
export default function CreatePostPage() {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [errors, setErrors] = 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 (
|
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
|
<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}
|
validationErrors={errors}
|
||||||
onReset={() => {
|
|
||||||
setTitle("");
|
|
||||||
setContent("");
|
|
||||||
}}
|
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -28,6 +193,7 @@ export default function CreatePostPage() {
|
||||||
title: "Please enter a valid title",
|
title: "Please enter a valid title",
|
||||||
content: "Please enter valid content",
|
content: "Please enter valid content",
|
||||||
});
|
});
|
||||||
|
toast.error("Please enter valid content");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +204,7 @@ export default function CreatePostPage() {
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
setErrors({ content: "Please enter valid content" });
|
setErrors({ content: "Please enter valid content" });
|
||||||
|
toast.error("Please enter valid content");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +213,19 @@ export default function CreatePostPage() {
|
||||||
return;
|
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(
|
const response = await fetch(
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||||
? "https://d2jam.com/api/v1/post"
|
? "https://d2jam.com/api/v1/post"
|
||||||
|
@ -53,25 +233,39 @@ export default function CreatePostPage() {
|
||||||
{
|
{
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: title,
|
title: title,
|
||||||
content: content,
|
content: sanitizedHtml,
|
||||||
|
sticky,
|
||||||
username: getCookie("user"),
|
username: getCookie("user"),
|
||||||
|
tags: [
|
||||||
|
...tags,
|
||||||
|
...(options
|
||||||
|
? options.filter((tag) => tag.isFixed).map((tag) => tag.id)
|
||||||
|
: []),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
authorization: `Bearer ${getCookie("token")}`,
|
authorization: `Bearer ${getCookie("token")}`,
|
||||||
},
|
},
|
||||||
|
credentials: "include",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status == 401) {
|
if (response.status == 401) {
|
||||||
setErrors({ content: "Invalid user" });
|
setErrors({ content: "Invalid user" });
|
||||||
|
setWaitingPost(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Successfully created post");
|
if (response.ok) {
|
||||||
|
toast.success("Successfully created post");
|
||||||
redirect("/");
|
setWaitingPost(false);
|
||||||
|
redirect("/");
|
||||||
|
} else {
|
||||||
|
toast.error("An error occured");
|
||||||
|
setWaitingPost(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -85,24 +279,51 @@ export default function CreatePostPage() {
|
||||||
onValueChange={setTitle}
|
onValueChange={setTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Editor content={content} setContent={setContent} />
|
||||||
isRequired
|
|
||||||
label="Content"
|
<Spacer />
|
||||||
labelPlacement="outside"
|
|
||||||
name="content"
|
{mounted && (
|
||||||
placeholder="Enter the post body"
|
<Select
|
||||||
value={content}
|
styles={styles}
|
||||||
onValueChange={setContent}
|
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">
|
<div className="flex gap-2">
|
||||||
<Button color="primary" type="submit">
|
<Button color="primary" type="submit">
|
||||||
Create
|
{waitingPost ? (
|
||||||
</Button>
|
<LoaderCircle className="animate-spin" size={16} />
|
||||||
<Button type="reset" variant="flat">
|
) : (
|
||||||
Reset
|
<p>Create</p>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
{!isMobile && (
|
||||||
|
<div className="flex flex-col gap-4 px-8 items-end">
|
||||||
|
<Timers />
|
||||||
|
<Streams />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Before Width: 48px | Height: 48px | Size: 15 KiB After Width: 48px | Height: 48px | 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 { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Navbar from "../components/navbar";
|
import Navbar from "../components/navbar";
|
||||||
import Providers from "./providers";
|
|
||||||
import { ToastContainer } from "react-toastify";
|
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"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Dare2Jam",
|
title: "Down2Jam",
|
||||||
description: "A community built game jam!",
|
description: "A community built game jam!",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,17 +20,23 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<Providers>
|
<NextUIProvider>
|
||||||
<div className="dark">
|
<ThemeProvider attribute="class">
|
||||||
<div className="bg-zinc-100 dark:bg-zinc-950 min-h-screen">
|
<div className="">
|
||||||
<Navbar />
|
<div className="bg-[#fff] dark:bg-[#181818] min-h-screen flex flex-col ease-in-out transition-color duration-500">
|
||||||
<div className="max-w-8xl mx-auto">{children}</div>
|
<Navbar />
|
||||||
<ToastContainer />
|
<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>
|
||||||
</div>
|
</ThemeProvider>
|
||||||
</Providers>
|
</NextUIProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"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 { redirect } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
@ -48,6 +48,7 @@ export default function UserPage() {
|
||||||
body: JSON.stringify({ username: username, password: password }),
|
body: JSON.stringify({ username: username, password: password }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -57,7 +58,14 @@ export default function UserPage() {
|
||||||
return;
|
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 = `token=${token}`;
|
||||||
document.cookie = `user=${user.slug}`;
|
document.cookie = `user=${user.slug}`;
|
||||||
|
@ -96,6 +104,9 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,11 +6,28 @@ import { toast } from "react-toastify";
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
useEffect(() => {
|
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 (
|
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 Posts from "@/components/posts";
|
||||||
import Timers from "@/components/timers";
|
import Timers from "@/components/timers";
|
||||||
|
import Streams from "@/components/streams";
|
||||||
|
import JamHeader from "@/components/jam-header";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="flex justify-between flex-wrap">
|
||||||
<div className="fixed left-0 top-0 w-full h-full z-0">
|
<div className="md:w-2/3">
|
||||||
<Image
|
<JamHeader />
|
||||||
src="/images/bg.jpg"
|
<Posts />
|
||||||
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>
|
</div>
|
||||||
<div className="z-10 relative flex w-full flex-wrap">
|
<div>
|
||||||
<div>
|
<Timers />
|
||||||
<div className="flex flex-col gap-4 py-4 sm:py-8 md:py-12 pl-16">
|
<Streams />
|
||||||
<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>
|
</div>
|
||||||
</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";
|
"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 { redirect } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
@ -74,6 +74,7 @@ export default function UserPage() {
|
||||||
body: JSON.stringify({ username: username, password: password }),
|
body: JSON.stringify({ username: username, password: password }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -131,6 +132,9 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[#333] dark:text-white transition-color duration-250">
|
||||||
|
Already have an account? <Link href="/login">Log In</Link>
|
||||||
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</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() {
|
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 (
|
return (
|
||||||
<div>
|
<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>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,429 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
|
||||||
Navbar as NavbarBase,
|
|
||||||
NavbarBrand,
|
|
||||||
NavbarContent,
|
|
||||||
NavbarItem,
|
|
||||||
} from "@nextui-org/navbar";
|
|
||||||
import { Link } from "@nextui-org/link";
|
|
||||||
import { Divider } from "@nextui-org/divider";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownTrigger,
|
|
||||||
Image,
|
|
||||||
Spacer,
|
|
||||||
Tooltip,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { SiDiscord, SiForgejo, SiGithub } from "@icons-pack/react-simple-icons";
|
|
||||||
import { LogInIcon, Menu, NotebookPen, SquarePen } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { hasCookie, getCookie } from "@/helpers/cookie";
|
import PCNavbar from "./PCNavbar";
|
||||||
import { usePathname } from "next/navigation";
|
import MobileNavbar from "./MobileNavbar";
|
||||||
import { UserType } from "@/types/UserType";
|
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [user, setUser] = useState<UserType>();
|
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||||
const pathname = usePathname();
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadUser();
|
|
||||||
async function loadUser() {
|
|
||||||
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")}` },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status == 200) {
|
|
||||||
setUser(await response.json());
|
|
||||||
} else {
|
|
||||||
setUser(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed
|
setIsMobile(window.innerWidth <= 768);
|
||||||
};
|
};
|
||||||
handleResize();
|
handleResize();
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return isMobile ? <MobileNavbar /> : <PCNavbar />;
|
||||||
<NavbarBase
|
|
||||||
shouldHideOnScroll
|
|
||||||
maxWidth="2xl"
|
|
||||||
className="bg-transparent p-1"
|
|
||||||
>
|
|
||||||
<NavbarBrand>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="duration-500 ease-in-out transition-all transform hover:scale-110"
|
|
||||||
>
|
|
||||||
<Image src="/images/dare2jam.png" alt="Dare2Jam logo" width={80} />
|
|
||||||
</Link>
|
|
||||||
</NavbarBrand>
|
|
||||||
<NavbarContent justify="end">
|
|
||||||
{isMobile ? (
|
|
||||||
user ? (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Avatar src={user.profilePicture} />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu className="text-black">
|
|
||||||
<DropdownItem key="create-post" href="/create-post">
|
|
||||||
Create Post
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
key="github"
|
|
||||||
href="https://github.com/Ategon/Jamjar"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
key="forgejo"
|
|
||||||
href="https://git.edikoyo.com/Ategon/Jamjar"
|
|
||||||
>
|
|
||||||
Forgejo
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
key="discord"
|
|
||||||
href="https://discord.gg/rfmKzM6ASw"
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="logout" color="danger" href="/logout">
|
|
||||||
Logout
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Menu />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu className="text-black">
|
|
||||||
<DropdownItem
|
|
||||||
key="github"
|
|
||||||
href="https://github.com/Ategon/Jamjar"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
key="forgejo"
|
|
||||||
href="https://git.edikoyo.com/Ategon/Jamjar"
|
|
||||||
>
|
|
||||||
Forgejo
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
key="discord"
|
|
||||||
href="https://discord.gg/rfmKzM6ASw"
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="login" href="/login">
|
|
||||||
Log In
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="signup" href="/signup">
|
|
||||||
Sign Up
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
{user && (
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/create-post">
|
|
||||||
<Button
|
|
||||||
endContent={<SquarePen />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Create Post
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Spacer x={32} />
|
|
||||||
</NavbarItem>
|
|
||||||
)}
|
|
||||||
<NavbarItem>
|
|
||||||
<Tooltip
|
|
||||||
delay={1000}
|
|
||||||
content={
|
|
||||||
<div className="px-1 py-2 text-black text-center">
|
|
||||||
<div className="text-small font-bold">GitHub</div>
|
|
||||||
<div className="text-tiny">Source Code</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://github.com/Ategon/Jamjar"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiGithub title="" />
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Tooltip
|
|
||||||
delay={1000}
|
|
||||||
content={
|
|
||||||
<div className="px-1 py-2 text-black text-center">
|
|
||||||
<div className="text-small font-bold">Forgejo</div>
|
|
||||||
<div className="text-tiny">Source Code</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://git.edikoyo.com/Ategon/Jamjar"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiForgejo title="" />
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Link
|
|
||||||
href="https://discord.gg/rfmKzM6ASw"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-indigo-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiDiscord />
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
<Divider orientation="vertical" className="h-1/2" />
|
|
||||||
{!user ? (
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/login">
|
|
||||||
<Button
|
|
||||||
endContent={<LogInIcon />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/signup">
|
|
||||||
<Button
|
|
||||||
endContent={<NotebookPen />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Avatar src={user.profilePicture} />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu>
|
|
||||||
{/* <DropdownItem
|
|
||||||
key="profile"
|
|
||||||
className="text-black"
|
|
||||||
href="/profile"
|
|
||||||
>
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</NavbarContent>
|
|
||||||
</NavbarBase>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
|
|
||||||
{isMobile ? (
|
|
||||||
// Mobile view
|
|
||||||
user ? (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Avatar src={user.profilePicture} />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownItem key="create-post" href="/create-post">
|
|
||||||
Create Post
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="logout" color="danger" href="/logout">
|
|
||||||
Logout
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Button auto flat className="text-white">
|
|
||||||
☰
|
|
||||||
</Button>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownItem key="github" href="https://github.com/Ategon/Jamjar" isExternal>
|
|
||||||
GitHub
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="forgejo" href="https://git.edikoyo.com/Ategon/Jamjar" isExternal>
|
|
||||||
Forgejo
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="discord" href="https://discord.gg/rfmKzM6ASw" isExternal>
|
|
||||||
Discord
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="login" href="/login">
|
|
||||||
Log In
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="signup" href="/signup">
|
|
||||||
Sign Up
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
|
|
||||||
|
|
||||||
user && (
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/create-post">
|
|
||||||
<Button
|
|
||||||
endContent={<SquarePen />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Create Post
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Spacer x={32} />
|
|
||||||
</NavbarItem>
|
|
||||||
)
|
|
||||||
<NavbarItem>
|
|
||||||
<Tooltip
|
|
||||||
delay={1000}
|
|
||||||
content={
|
|
||||||
<div className="px-1 py-2 text-black text-center">
|
|
||||||
<div className="text-small font-bold">GitHub</div>
|
|
||||||
<div className="text-tiny">Source Code</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://github.com/Ategon/Jamjar"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiGithub title="" />
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Tooltip
|
|
||||||
delay={1000}
|
|
||||||
content={
|
|
||||||
<div className="px-1 py-2 text-black text-center">
|
|
||||||
<div className="text-small font-bold">Forgejo</div>
|
|
||||||
<div className="text-tiny">Source Code</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="https://git.edikoyo.com/Ategon/Jamjar"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiForgejo title="" />
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Link
|
|
||||||
href="https://discord.gg/rfmKzM6ASw"
|
|
||||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-indigo-100"
|
|
||||||
isExternal
|
|
||||||
>
|
|
||||||
<SiDiscord />
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
<Divider orientation="vertical" className="h-1/2" />
|
|
||||||
{!user ? (
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/login">
|
|
||||||
<Button
|
|
||||||
endContent={<LogInIcon />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
<NavbarItem>
|
|
||||||
<Link href="/signup">
|
|
||||||
<Button
|
|
||||||
endContent={<NotebookPen />}
|
|
||||||
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
|
||||||
variant="bordered"
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</NavbarItem>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<Avatar src={user.profilePicture} />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu>
|
|
||||||
|
|
||||||
<DropdownItem
|
|
||||||
key="logout"
|
|
||||||
color="danger"
|
|
||||||
className="text-danger"
|
|
||||||
href="/logout"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
)
|
|
||||||
</NavbarContent> */
|
|
||||||
|
|
162
src/components/posts/CommentCard.tsx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import { CommentType } from "@/types/CommentType";
|
||||||
|
import { Avatar, Button, Card, CardBody, Spacer } from "@nextui-org/react";
|
||||||
|
import { formatDistance } from "date-fns";
|
||||||
|
import { LoaderCircle, Reply } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Editor from "../editor";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
import LikeButton from "./LikeButton";
|
||||||
|
|
||||||
|
export default function CommentCard({ comment }: { comment: CommentType }) {
|
||||||
|
const [creatingReply, setCreatingReply] = useState<boolean>(false);
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [waitingPost, setWaitingPost] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all">
|
||||||
|
<CardBody className="p-5">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
|
||||||
|
<p>By</p>
|
||||||
|
<Link
|
||||||
|
href={`/u/${comment.author.slug}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
className="w-6 h-6"
|
||||||
|
src={comment.author.profilePicture}
|
||||||
|
classNames={{
|
||||||
|
base: "bg-transparent",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p>{comment.author.name}</p>
|
||||||
|
</Link>
|
||||||
|
<p>
|
||||||
|
{formatDistance(new Date(comment.createdAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
|
||||||
|
dangerouslySetInnerHTML={{ __html: comment.content }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<LikeButton
|
||||||
|
likes={comment.likes.length}
|
||||||
|
liked={comment.hasLiked}
|
||||||
|
parentId={comment.id}
|
||||||
|
isComment
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
onPress={() => {
|
||||||
|
setCreatingReply(!creatingReply);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Reply size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
{creatingReply && (
|
||||||
|
<>
|
||||||
|
<Editor content={content} setContent={setContent} />
|
||||||
|
<div id="create-comment" />
|
||||||
|
<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"),
|
||||||
|
commentId: comment?.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 Reply</p>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Spacer y={4} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{comment.children.length > 0 &&
|
||||||
|
(comment.children[0].author ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{comment.children.map((comment) => (
|
||||||
|
<CommentCard key={comment.id} comment={comment} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
onPress={() => {
|
||||||
|
toast.warning("Feature coming soon");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load replies
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|