mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Compare commits
No commits in common. "f26af125b8706bcb36d701926eb28e072457e2e0" and "469c16fac6f3515be37dad8c5c8e40ec8db4dffc" have entirely different histories.
f26af125b8
...
469c16fac6
38 changed files with 178 additions and 4571 deletions
package-lock.jsonpackage.jsontailwind.config.ts
public/images
src
app
components
editor
footer
jam-header
link-components
navbar
posts
streams
theme-toggle
timers
types
3653
package-lock.json
generated
3653
package-lock.json
generated
File diff suppressed because it is too large
Load diff
44
package.json
44
package.json
|
@ -11,60 +11,18 @@
|
||||||
"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-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/bg.jpg
Normal file
BIN
public/images/bg.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.1 MiB |
BIN
public/images/d2jam.png
Normal file
BIN
public/images/d2jam.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 25 KiB |
BIN
public/images/dare2jam.png
Normal file
BIN
public/images/dare2jam.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 25 KiB |
Binary file not shown.
Before ![]() (image error) Size: 24 KiB |
|
@ -1,25 +1,25 @@
|
||||||
"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 } from "@nextui-org/react";
|
import { Button, Form, Input, Textarea } from "@nextui-org/react";
|
||||||
import { LoaderCircle } from "lucide-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";
|
||||||
import sanitizeHtml from "sanitize-html";
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
|
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
|
||||||
<Form
|
<Form
|
||||||
className="w-full max-w-2xl flex flex-col gap-4"
|
className="w-full max-w-xs flex flex-col gap-4"
|
||||||
validationErrors={errors}
|
validationErrors={errors}
|
||||||
|
onReset={() => {
|
||||||
|
setTitle("");
|
||||||
|
setContent("");
|
||||||
|
}}
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +38,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,9 +46,6 @@ export default function CreatePostPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedHtml = sanitizeHtml(content);
|
|
||||||
setWaitingPost(true);
|
|
||||||
|
|
||||||
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"
|
||||||
|
@ -58,7 +53,7 @@ export default function CreatePostPage() {
|
||||||
{
|
{
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: title,
|
title: title,
|
||||||
content: sanitizedHtml,
|
content: content,
|
||||||
username: getCookie("user"),
|
username: getCookie("user"),
|
||||||
}),
|
}),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -66,24 +61,17 @@ export default function CreatePostPage() {
|
||||||
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
toast.success("Successfully created post");
|
||||||
toast.success("Successfully created post");
|
|
||||||
setWaitingPost(false);
|
redirect("/");
|
||||||
redirect("/");
|
|
||||||
} else {
|
|
||||||
toast.error("An error occured");
|
|
||||||
setWaitingPost(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -97,15 +85,21 @@ export default function CreatePostPage() {
|
||||||
onValueChange={setTitle}
|
onValueChange={setTitle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Editor content={content} setContent={setContent} />
|
<Textarea
|
||||||
|
isRequired
|
||||||
|
label="Content"
|
||||||
|
labelPlacement="outside"
|
||||||
|
name="content"
|
||||||
|
placeholder="Enter the post body"
|
||||||
|
value={content}
|
||||||
|
onValueChange={setContent}
|
||||||
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button color="primary" type="submit">
|
<Button color="primary" type="submit">
|
||||||
{waitingPost ? (
|
Create
|
||||||
<LoaderCircle className="animate-spin" size={16} />
|
</Button>
|
||||||
) : (
|
<Button type="reset" variant="flat">
|
||||||
<p>Create</p>
|
Reset
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
Binary file not shown.
Before Width: 48px | Height: 48px | Size: 15 KiB After Width: 48px | Height: 48px | Size: 15 KiB |
|
@ -1,3 +0,0 @@
|
||||||
export default function InboxPage() {
|
|
||||||
return <p>Inbox page coming soon</p>;
|
|
||||||
}
|
|
|
@ -2,10 +2,10 @@ 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 { Spacer } from "@nextui-org/react";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import { ThemeProvider } from "next-themes";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
@ -20,23 +20,21 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<NextUIProvider>
|
<Providers>
|
||||||
<ThemeProvider attribute="class">
|
<div className="dark">
|
||||||
<div className="">
|
<div className="bg-gradient-to-br from-[#181818] to-[#222] min-h-screen flex flex-col">
|
||||||
<div className="bg-[#fff] dark:bg-[#181818] min-h-screen flex flex-col ease-in-out transition-color duration-500">
|
<Navbar />
|
||||||
<Navbar />
|
<Spacer y={5} />
|
||||||
<Spacer y={5} />
|
<div className="max-w-8xl mx-auto flex-grow w-full">
|
||||||
<div className="max-w-6xl xl:max-w-7xl 2xl:max-w-8xl mx-auto flex-grow w-full">
|
{children}
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
<ToastContainer />
|
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</div>
|
||||||
</NextUIProvider>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
<p>
|
||||||
Don't have an account? <Link href="/signup">Sign up</Link>
|
Don't have an account? <Link href="/signup">Sign up</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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";
|
import JamHeader from "@/components/jam-header";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
@ -12,7 +11,6 @@ export default async function Home() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Timers />
|
<Timers />
|
||||||
<Streams />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
12
src/app/providers.tsx
Normal file
12
src/app/providers.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {NextUIProvider} from "@nextui-org/react";
|
||||||
|
import {ReactNode} from "react";
|
||||||
|
|
||||||
|
export default function Providers({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<NextUIProvider>
|
||||||
|
{children}
|
||||||
|
</NextUIProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export default function ReportsPage() {
|
|
||||||
return <p>Reports page coming soon</p>;
|
|
||||||
}
|
|
|
@ -129,7 +129,7 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
<p>
|
||||||
Already have an account? <Link href="/login">Log In</Link>
|
Already have an account? <Link href="/login">Log In</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
"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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const limit = 32767;
|
|
||||||
|
|
||||||
export default function Editor({ content, setContent }: 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 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 transform-all duration-500 ease-in-out",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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-500 ease-in-out 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="transform-all duration-500 ease-in-out"
|
|
||||||
/>
|
|
||||||
<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="transform-all duration-500 ease-in-out"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{editor.storage.characterCount.characters()} / {limit} characters
|
|
||||||
<br />
|
|
||||||
{editor.storage.characterCount.words()} words
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ import { SiDiscord, SiForgejo, SiGithub } from "@icons-pack/react-simple-icons";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
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="p-8 bg-[#222] mt-8 border-t-2 border-white/15">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
export default function JamHeader() {
|
export default function JamHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#7090b9] dark:bg-[#124a88] flex rounded-2xl overflow-hidden text-white transition-color duration-500 ease-in-out">
|
<div className="bg-[#124a88] flex rounded-2xl overflow-hidden text-white">
|
||||||
<div className="bg-[#85bdd2] dark:bg-[#1892b3] p-4 px-6 flex items-center gap-2 font-bold transition-color duration-500 ease-in-out">
|
<div className="bg-[#1892b3] p-4 px-6 flex items-center gap-2 font-bold">
|
||||||
<Calendar />
|
<Calendar />
|
||||||
<p>Dare2Jam 1</p>
|
<p>Dare2Jam 1</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default function ButtonAction({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
endContent={icon}
|
endContent={icon}
|
||||||
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
className="text-white border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,11 +11,11 @@ export default function ButtonLink({ icon, href, name }: ButtonLinkProps) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
|
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110 ease-in-out"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
endContent={icon}
|
endContent={icon}
|
||||||
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out"
|
className="text-white border-white/50 transition-all transform duration-500 ease-in-out"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default function IconLink({ icon, href }: IconLinkProps) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 duration-500 ease-in-out transition-color"
|
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default function Link({ name, href }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
<BaseLink
|
<BaseLink
|
||||||
href={href}
|
href={href}
|
||||||
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110 duration-500 ease-in-out transition-color"
|
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</BaseLink>
|
</BaseLink>
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default function MobileNavbar() {
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
as={NextImage}
|
as={NextImage}
|
||||||
src="/images/hereyougopomo.png"
|
src="/images/d2jam.png"
|
||||||
className="min-w-[70px]"
|
className="min-w-[70px]"
|
||||||
alt="Dare2Jam logo"
|
alt="Dare2Jam logo"
|
||||||
width={70}
|
width={70}
|
||||||
|
|
|
@ -29,13 +29,7 @@ export default function MobileNavbarUser({
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar
|
<Avatar src={user.profilePicture} className="cursor-pointer" />
|
||||||
src={user.profilePicture}
|
|
||||||
className="cursor-pointer"
|
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
{jam && isInJam ? (
|
{jam && isInJam ? (
|
||||||
|
|
|
@ -3,12 +3,7 @@ import { Input, NavbarItem } from "@nextui-org/react";
|
||||||
export default function NavbarSearchbar() {
|
export default function NavbarSearchbar() {
|
||||||
return (
|
return (
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<Input
|
<Input placeholder="Search" />
|
||||||
placeholder="Search"
|
|
||||||
classNames={{
|
|
||||||
inputWrapper: "!duration-500 ease-in-out transition-all",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,10 @@ import NavbarLink from "./NavbarLink";
|
||||||
import NavbarSearchbar from "./NavbarSearchbar";
|
import NavbarSearchbar from "./NavbarSearchbar";
|
||||||
import NavbarButtonLink from "./NavbarButtonLink";
|
import NavbarButtonLink from "./NavbarButtonLink";
|
||||||
import {
|
import {
|
||||||
Bell,
|
|
||||||
CalendarPlus,
|
CalendarPlus,
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
NotebookPen,
|
NotebookPen,
|
||||||
Shield,
|
|
||||||
SquarePen,
|
SquarePen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import NextImage from "next/image";
|
import NextImage from "next/image";
|
||||||
|
@ -30,8 +28,6 @@ import { UserType } from "@/types/UserType";
|
||||||
import NavbarUser from "./PCNavbarUser";
|
import NavbarUser from "./PCNavbarUser";
|
||||||
import NavbarButtonAction from "./NavbarButtonAction";
|
import NavbarButtonAction from "./NavbarButtonAction";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import NavbarIconLink from "./NavbarIconLink";
|
|
||||||
import ThemeToggle from "../theme-toggle";
|
|
||||||
|
|
||||||
export default function PCNavbar() {
|
export default function PCNavbar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
@ -80,12 +76,7 @@ export default function PCNavbar() {
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarBase
|
<NavbarBase maxWidth="2xl" className="bg-[#222] p-1" isBordered height={80}>
|
||||||
maxWidth="2xl"
|
|
||||||
className="bg-[#fff] dark:bg-[#222] p-1 duration-500 ease-in-out transition-color"
|
|
||||||
isBordered
|
|
||||||
height={80}
|
|
||||||
>
|
|
||||||
{/* Left side navbar items */}
|
{/* Left side navbar items */}
|
||||||
<NavbarContent justify="start" className="gap-10">
|
<NavbarContent justify="start" className="gap-10">
|
||||||
<NavbarBrand className="flex-grow-0">
|
<NavbarBrand className="flex-grow-0">
|
||||||
|
@ -95,11 +86,11 @@ export default function PCNavbar() {
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
as={NextImage}
|
as={NextImage}
|
||||||
src="/images/hereyougopomo.png"
|
src="/images/d2jam.png"
|
||||||
className="min-w-[70px]"
|
className="min-w-[70px]"
|
||||||
alt="Dare2Jam logo"
|
alt="Dare2Jam logo"
|
||||||
width={70}
|
width={70}
|
||||||
height={70}
|
height={59.7}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
@ -143,11 +134,6 @@ export default function PCNavbar() {
|
||||||
href="/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" />
|
<Divider orientation="vertical" className="h-1/2" />
|
||||||
{!user && (
|
{!user && (
|
||||||
<NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" />
|
<NavbarButtonLink icon={<LogInIcon />} name="Log In" href="/login" />
|
||||||
|
|
|
@ -17,13 +17,7 @@ export default function PCNavbarUser({ user }: NavbarUserProps) {
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar
|
<Avatar src={user.profilePicture} className="cursor-pointer" />
|
||||||
src={user.profilePicture}
|
|
||||||
className="cursor-pointer"
|
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
|
|
@ -2,37 +2,20 @@
|
||||||
|
|
||||||
import { Button } from "@nextui-org/react";
|
import { Button } from "@nextui-org/react";
|
||||||
import { PostType } from "@/types/PostType";
|
import { PostType } from "@/types/PostType";
|
||||||
import { Heart, LoaderCircle } from "lucide-react";
|
import { Heart } from "lucide-react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getCookie } from "@/helpers/cookie";
|
import { getCookie } from "@/helpers/cookie";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
export default function LikeButton({ post }: { post: PostType }) {
|
export default function LikeButton({ post }: { post: PostType }) {
|
||||||
const [likes, setLikes] = useState<number>(post.likes.length);
|
const [likes, setLikes] = useState<number>(post.likes.length);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [liked, setLiked] = useState<boolean>(false);
|
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
style={{
|
|
||||||
color: post.hasLiked ? (theme == "dark" ? "#5ed4f7" : "#05b7eb") : "",
|
|
||||||
borderColor: post.hasLiked
|
|
||||||
? theme == "dark"
|
|
||||||
? "#5ed4f744"
|
|
||||||
: "#05b7eb44"
|
|
||||||
: "",
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (loading || liked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
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/like"
|
? "https://d2jam.com/api/v1/like"
|
||||||
|
@ -52,7 +35,6 @@ export default function LikeButton({ post }: { post: PostType }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setLoading(false);
|
|
||||||
if (response.status == 401) {
|
if (response.status == 401) {
|
||||||
redirect("/login");
|
redirect("/login");
|
||||||
} else {
|
} else {
|
||||||
|
@ -60,36 +42,11 @@ export default function LikeButton({ post }: { post: PostType }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
setLikes(parseInt(await response.text()));
|
||||||
setLikes(parseInt(data.likes));
|
|
||||||
post.hasLiked = data.action === "like";
|
|
||||||
setLoading(false);
|
|
||||||
setLiked(data.action === "like");
|
|
||||||
setTimeout(() => setLiked(false), 1000);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? (
|
<Heart size={16} /> {likes}
|
||||||
<LoaderCircle className="animate-spin" size={16} />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="flex gap-2 items-center"
|
|
||||||
style={{ position: "relative" }}
|
|
||||||
>
|
|
||||||
<Heart size={16} />
|
|
||||||
<Heart
|
|
||||||
size={16}
|
|
||||||
className={liked ? "animate-ping absolute top-0 left-0" : ""}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "0",
|
|
||||||
left: "0",
|
|
||||||
zIndex: "10",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p>{likes}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,285 +1,59 @@
|
||||||
import {
|
import { Avatar, Button, Card, CardBody, Spacer } from "@nextui-org/react";
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownSection,
|
|
||||||
DropdownTrigger,
|
|
||||||
Spacer,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PostType } from "@/types/PostType";
|
import { PostType } from "@/types/PostType";
|
||||||
import {
|
import { MessageCircle } from "lucide-react";
|
||||||
Flag,
|
|
||||||
MessageCircle,
|
|
||||||
Minus,
|
|
||||||
MoreVertical,
|
|
||||||
Plus,
|
|
||||||
Shield,
|
|
||||||
ShieldAlert,
|
|
||||||
ShieldX,
|
|
||||||
Trash,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import LikeButton from "./LikeButton";
|
import LikeButton from "./LikeButton";
|
||||||
import { PostStyle } from "@/types/PostStyle";
|
import { PostStyle } from "@/types/PostStyle";
|
||||||
import { UserType } from "@/types/UserType";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { getCookie } from "@/helpers/cookie";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
export default function PostCard({
|
export default function PostCard({
|
||||||
post,
|
post,
|
||||||
style,
|
style,
|
||||||
user,
|
|
||||||
}: {
|
}: {
|
||||||
post: PostType;
|
post: PostType;
|
||||||
style: PostStyle;
|
style: PostStyle;
|
||||||
user?: UserType;
|
|
||||||
}) {
|
}) {
|
||||||
const [minimized, setMinimized] = useState<boolean>(false);
|
|
||||||
const [hidden, setHidden] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className="bg-opacity-60">
|
||||||
className="bg-opacity-60 !duration-500 ease-in-out !transition-all"
|
|
||||||
style={{ display: hidden ? "none" : "flex" }}
|
|
||||||
>
|
|
||||||
<CardBody className="p-5">
|
<CardBody className="p-5">
|
||||||
{(style == "cozy" || style == "adaptive") &&
|
{style == "cozy" && (
|
||||||
(minimized ? (
|
<div>
|
||||||
<div className="flex items-center justify-between">
|
<p className="text-2xl">{post.title}</p>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<p>{post.title}</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
|
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
|
||||||
<p>By</p>
|
<p>By</p>
|
||||||
<Link
|
<Link
|
||||||
href={`/u/${post.author.slug}`}
|
href={`/u/${post.author.slug}`}
|
||||||
className="flex items-center gap-2"
|
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>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
isIconOnly
|
|
||||||
onPress={() => setMinimized(false)}
|
|
||||||
>
|
>
|
||||||
<Plus size={16} />
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
className="w-6 h-6"
|
||||||
|
src={post.author.profilePicture}
|
||||||
|
/>
|
||||||
|
<p>{post.author.name}</p>
|
||||||
|
</Link>
|
||||||
|
<p>
|
||||||
|
{formatDistance(new Date(post.createdAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<p>{post.content}</p>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<LikeButton post={post} />
|
||||||
|
<Button size="sm" variant="bordered">
|
||||||
|
<MessageCircle size={16} /> {0}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div>
|
)}
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<p className="text-2xl">{post.title}</p>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
isIconOnly
|
|
||||||
onPress={() => setMinimized(true)}
|
|
||||||
>
|
|
||||||
<Minus size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 transition-all !duration-500 ease-in-out"
|
|
||||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Spacer y={4} />
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<LikeButton post={post} />
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
onPress={() => {
|
|
||||||
toast.warning("Comment functionality coming soon");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MessageCircle size={16} /> {0}
|
|
||||||
</Button>
|
|
||||||
<Dropdown>
|
|
||||||
<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");
|
|
||||||
setHidden(true);
|
|
||||||
} else {
|
|
||||||
toast.error("Error while deleting post");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</DropdownSection>
|
|
||||||
<DropdownSection title="Mod Zone">
|
|
||||||
{user?.mod ? (
|
|
||||||
<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");
|
|
||||||
setHidden(true);
|
|
||||||
} else {
|
|
||||||
toast.error("Error while removing post");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{user?.admin && !post.author.mod ? (
|
|
||||||
<DropdownItem
|
|
||||||
key="promote-mod"
|
|
||||||
startContent={<Shield />}
|
|
||||||
description="Promote user to Mod"
|
|
||||||
>
|
|
||||||
Appoint as mod
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{user?.admin &&
|
|
||||||
post.author.mod &&
|
|
||||||
post.author.id !== user.id ? (
|
|
||||||
<DropdownItem
|
|
||||||
key="demote-mod"
|
|
||||||
startContent={<ShieldX />}
|
|
||||||
description="Demote user from Mod"
|
|
||||||
>
|
|
||||||
Remove as mod
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{user?.admin && !post.author.admin ? (
|
|
||||||
<DropdownItem
|
|
||||||
key="promote-admin"
|
|
||||||
startContent={<ShieldAlert />}
|
|
||||||
description="Promote user to Admin"
|
|
||||||
>
|
|
||||||
Appoint as admin
|
|
||||||
</DropdownItem>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</DropdownSection>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{style == "compact" && (
|
{style == "compact" && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl">{post.title}</p>
|
<p className="text-2xl">{post.title}</p>
|
||||||
|
@ -294,9 +68,6 @@ export default function PostCard({
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
src={post.author.profilePicture}
|
src={post.author.profilePicture}
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<p>{post.author.name}</p>
|
<p>{post.author.name}</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -322,9 +93,6 @@ export default function PostCard({
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
src={post.author.profilePicture}
|
src={post.author.profilePicture}
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<p>{post.author.name}</p>
|
<p>{post.author.name}</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -336,6 +104,44 @@ export default function PostCard({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{style == "adaptive" && (
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl">{post.title}</p>
|
||||||
|
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
<p>{post.author.name}</p>
|
||||||
|
</Link>
|
||||||
|
<p>
|
||||||
|
{formatDistance(new Date(post.createdAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<p>{post.content}</p>
|
||||||
|
|
||||||
|
<Spacer y={4} />
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<LikeButton post={post} />
|
||||||
|
<Button size="sm" variant="bordered">
|
||||||
|
<MessageCircle size={16} /> {0}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,59 +12,23 @@ import {
|
||||||
} from "@nextui-org/react";
|
} from "@nextui-org/react";
|
||||||
import { PostSort } from "@/types/PostSort";
|
import { PostSort } from "@/types/PostSort";
|
||||||
import { PostStyle } from "@/types/PostStyle";
|
import { PostStyle } from "@/types/PostStyle";
|
||||||
import { getCookie } from "@/helpers/cookie";
|
|
||||||
import { UserType } from "@/types/UserType";
|
|
||||||
import { LoaderCircle } from "lucide-react";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
export default function Posts() {
|
export default function Posts() {
|
||||||
const [posts, setPosts] = useState<PostType[]>();
|
const [posts, setPosts] = useState<PostType[]>();
|
||||||
const [sort, setSort] = useState<PostSort>("newest");
|
const [sort, setSort] = useState<PostSort>("newest");
|
||||||
const [style, setStyle] = useState<PostStyle>("cozy");
|
const [style, setStyle] = useState<PostStyle>("cozy");
|
||||||
const [user, setUser] = useState<UserType>();
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadUserAndPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
setLoading(true);
|
const response = await fetch(
|
||||||
// Fetch the user
|
|
||||||
const userResponse = await fetch(
|
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||||
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
|
? `https://d2jam.com/api/v1/posts?sort=${sort}`
|
||||||
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
|
: `http://localhost:3005/api/v1/posts?sort=${sort}`
|
||||||
{
|
|
||||||
headers: { authorization: `Bearer ${getCookie("token")}` },
|
|
||||||
credentials: "include",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
setPosts(await response.json());
|
||||||
if (userResponse.ok) {
|
|
||||||
const userData = await userResponse.json();
|
|
||||||
setUser(userData);
|
|
||||||
|
|
||||||
// Fetch posts with userSlug if user is available
|
|
||||||
const postsResponse = await fetch(
|
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
|
||||||
? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${userData.slug}`
|
|
||||||
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}`
|
|
||||||
);
|
|
||||||
setPosts(await postsResponse.json());
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
setUser(undefined);
|
|
||||||
|
|
||||||
// Fetch posts without userSlug if user is not available
|
|
||||||
const postsResponse = await fetch(
|
|
||||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
|
||||||
? `https://d2jam.com/api/v1/posts?sort=${sort}`
|
|
||||||
: `http://localhost:3005/api/v1/posts?sort=${sort}`
|
|
||||||
);
|
|
||||||
setPosts(await postsResponse.json());
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadUserAndPosts();
|
fetchPosts();
|
||||||
}, [sort]);
|
}, [sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -73,11 +37,7 @@ export default function Posts() {
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button size="sm" className="text-xs" variant="faded">
|
||||||
size="sm"
|
|
||||||
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
|
|
||||||
variant="faded"
|
|
||||||
>
|
|
||||||
{sort.charAt(0).toUpperCase() + sort.slice(1)}
|
{sort.charAt(0).toUpperCase() + sort.slice(1)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
@ -85,32 +45,21 @@ export default function Posts() {
|
||||||
onAction={(key) => {
|
onAction={(key) => {
|
||||||
setSort(key as PostSort);
|
setSort(key as PostSort);
|
||||||
}}
|
}}
|
||||||
className="text-[#333] dark:text-white"
|
className="text-black"
|
||||||
>
|
>
|
||||||
<DropdownItem key="newest">Newest</DropdownItem>
|
<DropdownItem key="newest">Newest</DropdownItem>
|
||||||
<DropdownItem key="top">Top</DropdownItem>
|
<DropdownItem key="top">Top</DropdownItem>
|
||||||
<DropdownItem key="oldest">Oldest</DropdownItem>
|
<DropdownItem key="oldest">Oldest</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button
|
<Button size="sm" className="text-xs" variant="faded">
|
||||||
size="sm"
|
|
||||||
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
|
|
||||||
variant="faded"
|
|
||||||
onPress={() => {
|
|
||||||
toast.warning("Flair filtering functionality coming soon");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
All Tags
|
All Tags
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button size="sm" className="text-xs" variant="faded">
|
||||||
size="sm"
|
|
||||||
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
|
|
||||||
variant="faded"
|
|
||||||
>
|
|
||||||
{style.charAt(0).toUpperCase() + style.slice(1)}
|
{style.charAt(0).toUpperCase() + style.slice(1)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
@ -118,7 +67,7 @@ export default function Posts() {
|
||||||
onAction={(key) => {
|
onAction={(key) => {
|
||||||
setStyle(key as PostStyle);
|
setStyle(key as PostStyle);
|
||||||
}}
|
}}
|
||||||
className="text-[#333] dark:text-white"
|
className="text-black"
|
||||||
>
|
>
|
||||||
<DropdownItem key="cozy">Cozy</DropdownItem>
|
<DropdownItem key="cozy">Cozy</DropdownItem>
|
||||||
<DropdownItem key="compact">Compact</DropdownItem>
|
<DropdownItem key="compact">Compact</DropdownItem>
|
||||||
|
@ -128,22 +77,14 @@ export default function Posts() {
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 p-4">
|
||||||
{loading ? (
|
{posts &&
|
||||||
<div className="flex justify-center p-6">
|
posts.map((post) => (
|
||||||
<LoaderCircle
|
<div key={post.id}>
|
||||||
className="animate-spin text-[#333] dark:text-[#999]"
|
<PostCard post={post} style={style} />
|
||||||
size={24}
|
</div>
|
||||||
/>
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-3 p-4">
|
|
||||||
{posts &&
|
|
||||||
posts.map((post) => (
|
|
||||||
<PostCard key={post.id} post={post} style={style} user={user} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
|
|
|
@ -70,10 +70,10 @@ export default function Streams() {
|
||||||
alt={`${currentStreamer.userName}'s thumbnail`}
|
alt={`${currentStreamer.userName}'s thumbnail`}
|
||||||
style={{ width: "100%", borderRadius: "4px", marginBottom: "10px" }}
|
style={{ width: "100%", borderRadius: "4px", marginBottom: "10px" }}
|
||||||
/>
|
/>
|
||||||
<a href={`https://twitch.tv/${currentStreamer.userName}`} target="_blank" ><div style={{height:"100px",display:"flex", flexDirection:"column",justifyContent:"center"}}>
|
<div style={{height:"100px",display:"flex", flexDirection:"column",justifyContent:"center"}}>
|
||||||
<h3>{currentStreamer.userName}</h3>
|
<h3>{currentStreamer.userName}</h3>
|
||||||
<p>{currentStreamer.streamTitle}</p>
|
<p>{currentStreamer.streamTitle}</p>
|
||||||
</div></a>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{currentStreamer.streamTags.map((tag, index) => (
|
{currentStreamer.streamTags.map((tag, index) => (
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Moon, Sun } from "lucide-react";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
export default function ThemeToggle() {
|
|
||||||
const [isSpinning, setIsSpinning] = useState<boolean>(false);
|
|
||||||
const { setTheme, resolvedTheme } = useTheme();
|
|
||||||
const [mounted, setMounted] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
if (isSpinning) return;
|
|
||||||
|
|
||||||
setIsSpinning(true);
|
|
||||||
setTimeout(() => setIsSpinning(false), 500);
|
|
||||||
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={handleToggle}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
className={`${isSpinning && "animate-[spin_0.5s_ease-out]"} `}
|
|
||||||
>
|
|
||||||
<div className="duration-500 ease-in-out transition-all transform text-[#333] dark:text-white hover:scale-125">
|
|
||||||
{resolvedTheme === "dark" && <Moon />}
|
|
||||||
{resolvedTheme === "light" && <Sun />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ import Timer from "./Timer";
|
||||||
|
|
||||||
export default function Timers() {
|
export default function Timers() {
|
||||||
return (
|
return (
|
||||||
<div className="text-[#333] dark:text-white ease-in-out transition-color duration-500">
|
<div className="text-white">
|
||||||
<Timer
|
<Timer
|
||||||
name="Jam Start"
|
name="Jam Start"
|
||||||
targetDate={new Date("2025-04-04T18:00:00-05:00")}
|
targetDate={new Date("2025-04-04T18:00:00-05:00")}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { UserType } from "./UserType";
|
|
||||||
|
|
||||||
export interface PostType {
|
export interface PostType {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
author: UserType;
|
author: {
|
||||||
|
slug: string;
|
||||||
|
profilePicture: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
likes: [];
|
likes: [];
|
||||||
hasLiked: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,4 @@ export interface UserType {
|
||||||
name: string;
|
name: string;
|
||||||
profilePicture: string;
|
profilePicture: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
mod: boolean;
|
|
||||||
admin: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,6 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
plugins: [nextui(), require("@tailwindcss/typography")],
|
plugins: [nextui()],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|
Loading…
Reference in a new issue