mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Light theme and wysiwyg
This commit is contained in:
parent
469c16fac6
commit
50ee8098c4
30 changed files with 4565 additions and 173 deletions
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,18 +11,60 @@
|
|||
"dependencies": {
|
||||
"@icons-pack/react-simple-icons": "^11.0.1",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tiptap/extension-blockquote": "^2.11.2",
|
||||
"@tiptap/extension-bold": "^2.11.2",
|
||||
"@tiptap/extension-bullet-list": "^2.11.2",
|
||||
"@tiptap/extension-character-count": "^2.11.2",
|
||||
"@tiptap/extension-code-block": "^2.11.2",
|
||||
"@tiptap/extension-document": "^2.11.2",
|
||||
"@tiptap/extension-dropcursor": "^2.11.2",
|
||||
"@tiptap/extension-hard-break": "^2.11.2",
|
||||
"@tiptap/extension-heading": "^2.11.2",
|
||||
"@tiptap/extension-highlight": "^2.11.2",
|
||||
"@tiptap/extension-history": "^2.11.2",
|
||||
"@tiptap/extension-horizontal-rule": "^2.11.2",
|
||||
"@tiptap/extension-image": "^2.11.2",
|
||||
"@tiptap/extension-italic": "^2.11.2",
|
||||
"@tiptap/extension-list-item": "^2.11.2",
|
||||
"@tiptap/extension-ordered-list": "^2.11.2",
|
||||
"@tiptap/extension-paragraph": "^2.11.2",
|
||||
"@tiptap/extension-strike": "^2.11.2",
|
||||
"@tiptap/extension-subscript": "^2.11.2",
|
||||
"@tiptap/extension-superscript": "^2.11.2",
|
||||
"@tiptap/extension-table": "^2.11.2",
|
||||
"@tiptap/extension-table-cell": "^2.11.2",
|
||||
"@tiptap/extension-table-header": "^2.11.2",
|
||||
"@tiptap/extension-table-row": "^2.11.2",
|
||||
"@tiptap/extension-task-item": "^2.11.2",
|
||||
"@tiptap/extension-task-list": "^2.11.2",
|
||||
"@tiptap/extension-text": "^2.11.2",
|
||||
"@tiptap/extension-text-align": "^2.11.2",
|
||||
"@tiptap/extension-typography": "^2.11.2",
|
||||
"@tiptap/extension-underline": "^2.11.2",
|
||||
"@tiptap/extension-youtube": "^2.11.2",
|
||||
"@tiptap/pm": "^2.11.2",
|
||||
"@tiptap/react": "^2.11.2",
|
||||
"@tiptap/starter-kit": "^2.11.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.11.9",
|
||||
"i": "^0.3.7",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"next": "15.0.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"npm": "^11.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-toastify": "^11.0.3"
|
||||
"react-toastify": "^11.0.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"tiptap-markdown": "^0.8.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.1",
|
||||
"postcss": "^8",
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import Editor from "@/components/editor";
|
||||
import { getCookie, hasCookie } from "@/helpers/cookie";
|
||||
import { Button, Form, Input, Textarea } from "@nextui-org/react";
|
||||
import { Button, Form, Input } from "@nextui-org/react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
export default function CreatePostPage() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [errors, setErrors] = useState({});
|
||||
const [waitingPost, setWaitingPost] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
|
||||
<Form
|
||||
className="w-full max-w-xs flex flex-col gap-4"
|
||||
className="w-full max-w-2xl flex flex-col gap-4"
|
||||
validationErrors={errors}
|
||||
onReset={() => {
|
||||
setTitle("");
|
||||
setContent("");
|
||||
}}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -28,6 +28,7 @@ export default function CreatePostPage() {
|
|||
title: "Please enter a valid title",
|
||||
content: "Please enter valid content",
|
||||
});
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -38,6 +39,7 @@ export default function CreatePostPage() {
|
|||
|
||||
if (!content) {
|
||||
setErrors({ content: "Please enter valid content" });
|
||||
toast.error("Please enter valid content");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,9 @@ export default function CreatePostPage() {
|
|||
return;
|
||||
}
|
||||
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
setWaitingPost(true);
|
||||
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/post"
|
||||
|
@ -53,7 +58,7 @@ export default function CreatePostPage() {
|
|||
{
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
content: content,
|
||||
content: sanitizedHtml,
|
||||
username: getCookie("user"),
|
||||
}),
|
||||
method: "POST",
|
||||
|
@ -61,17 +66,24 @@ export default function CreatePostPage() {
|
|||
"Content-Type": "application/json",
|
||||
authorization: `Bearer ${getCookie("token")}`,
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status == 401) {
|
||||
setErrors({ content: "Invalid user" });
|
||||
setWaitingPost(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Successfully created post");
|
||||
|
||||
redirect("/");
|
||||
if (response.ok) {
|
||||
toast.success("Successfully created post");
|
||||
setWaitingPost(false);
|
||||
redirect("/");
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
setWaitingPost(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
@ -85,21 +97,15 @@ export default function CreatePostPage() {
|
|||
onValueChange={setTitle}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
isRequired
|
||||
label="Content"
|
||||
labelPlacement="outside"
|
||||
name="content"
|
||||
placeholder="Enter the post body"
|
||||
value={content}
|
||||
onValueChange={setContent}
|
||||
/>
|
||||
<Editor content={content} setContent={setContent} />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="primary" type="submit">
|
||||
Create
|
||||
</Button>
|
||||
<Button type="reset" variant="flat">
|
||||
Reset
|
||||
{waitingPost ? (
|
||||
<LoaderCircle className="animate-spin" size={16} />
|
||||
) : (
|
||||
<p>Create</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
3
src/app/inbox/page.tsx
Normal file
3
src/app/inbox/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
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 "./globals.css";
|
||||
import Navbar from "../components/navbar";
|
||||
import Providers from "./providers";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { NextUIProvider, Spacer } from "@nextui-org/react";
|
||||
import Footer from "@/components/footer";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
@ -20,21 +20,23 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>
|
||||
<div className="dark">
|
||||
<div className="bg-gradient-to-br from-[#181818] to-[#222] min-h-screen flex flex-col">
|
||||
<Navbar />
|
||||
<Spacer y={5} />
|
||||
<div className="max-w-8xl mx-auto flex-grow w-full">
|
||||
{children}
|
||||
<NextUIProvider>
|
||||
<ThemeProvider attribute="class">
|
||||
<div className="">
|
||||
<div className="bg-[#fff] dark:bg-[#181818] min-h-screen flex flex-col ease-in-out transition-color duration-500">
|
||||
<Navbar />
|
||||
<Spacer y={5} />
|
||||
<div className="max-w-6xl xl:max-w-7xl 2xl:max-w-8xl mx-auto flex-grow w-full">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
<ToastContainer />
|
||||
</div>
|
||||
<Footer />
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</div>
|
||||
</Providers>
|
||||
</ThemeProvider>
|
||||
</NextUIProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
@ -105,7 +105,7 @@ export default function UserPage() {
|
|||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<p>
|
||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
||||
Don't have an account? <Link href="/signup">Sign up</Link>
|
||||
</p>
|
||||
</Form>
|
||||
|
|
|
@ -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
3
src/app/reports/page.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function ReportsPage() {
|
||||
return <p>Reports page coming soon</p>;
|
||||
}
|
|
@ -129,7 +129,7 @@ export default function UserPage() {
|
|||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<p>
|
||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
||||
Already have an account? <Link href="/login">Log In</Link>
|
||||
</p>
|
||||
</Form>
|
||||
|
|
144
src/components/editor/EditorMenuBar.tsx
Normal file
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
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>
|
||||
);
|
||||
}
|
159
src/components/editor/index.tsx
Normal file
159
src/components/editor/index.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
"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 = 200;
|
||||
|
||||
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() {
|
||||
return (
|
||||
<div className="p-8 bg-[#222] mt-8 border-t-2 border-white/15">
|
||||
<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">
|
||||
|
|
|
@ -2,8 +2,8 @@ import { Calendar } from "lucide-react";
|
|||
|
||||
export default function JamHeader() {
|
||||
return (
|
||||
<div className="bg-[#124a88] flex rounded-2xl overflow-hidden text-white">
|
||||
<div className="bg-[#1892b3] p-4 px-6 flex items-center gap-2 font-bold">
|
||||
<div className="bg-[#7090b9] dark:bg-[#124a88] flex rounded-2xl overflow-hidden text-white transition-color duration-500 ease-in-out">
|
||||
<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">
|
||||
<Calendar />
|
||||
<p>Dare2Jam 1</p>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ export default function ButtonAction({
|
|||
return (
|
||||
<Button
|
||||
endContent={icon}
|
||||
className="text-white border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
||||
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
|
||||
variant="bordered"
|
||||
onPress={onPress}
|
||||
>
|
||||
|
|
|
@ -11,11 +11,11 @@ export default function ButtonLink({ icon, href, name }: ButtonLinkProps) {
|
|||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110 ease-in-out"
|
||||
className="flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
|
||||
>
|
||||
<Button
|
||||
endContent={icon}
|
||||
className="text-white border-white/50 transition-all transform duration-500 ease-in-out"
|
||||
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out"
|
||||
variant="bordered"
|
||||
>
|
||||
{name}
|
||||
|
|
|
@ -10,7 +10,7 @@ export default function IconLink({ icon, href }: IconLinkProps) {
|
|||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125"
|
||||
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"
|
||||
>
|
||||
{icon}
|
||||
</Link>
|
||||
|
|
|
@ -9,7 +9,7 @@ export default function Link({ name, href }: LinkProps) {
|
|||
return (
|
||||
<BaseLink
|
||||
href={href}
|
||||
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
|
||||
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"
|
||||
>
|
||||
{name}
|
||||
</BaseLink>
|
||||
|
|
|
@ -29,7 +29,13 @@ export default function MobileNavbarUser({
|
|||
<NavbarItem>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Avatar src={user.profilePicture} className="cursor-pointer" />
|
||||
<Avatar
|
||||
src={user.profilePicture}
|
||||
className="cursor-pointer"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
{jam && isInJam ? (
|
||||
|
|
|
@ -3,7 +3,12 @@ import { Input, NavbarItem } from "@nextui-org/react";
|
|||
export default function NavbarSearchbar() {
|
||||
return (
|
||||
<NavbarItem>
|
||||
<Input placeholder="Search" />
|
||||
<Input
|
||||
placeholder="Search"
|
||||
classNames={{
|
||||
inputWrapper: "!duration-500 ease-in-out transition-all",
|
||||
}}
|
||||
/>
|
||||
</NavbarItem>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,12 @@ 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";
|
||||
|
@ -28,6 +30,8 @@ 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();
|
||||
|
@ -76,7 +80,12 @@ export default function PCNavbar() {
|
|||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<NavbarBase maxWidth="2xl" className="bg-[#222] p-1" isBordered height={80}>
|
||||
<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">
|
||||
|
@ -134,6 +143,11 @@ export default function PCNavbar() {
|
|||
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" />
|
||||
|
|
|
@ -17,7 +17,13 @@ export default function PCNavbarUser({ user }: NavbarUserProps) {
|
|||
<NavbarItem>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Avatar src={user.profilePicture} className="cursor-pointer" />
|
||||
<Avatar
|
||||
src={user.profilePicture}
|
||||
className="cursor-pointer"
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
|
|
|
@ -2,20 +2,37 @@
|
|||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { PostType } from "@/types/PostType";
|
||||
import { Heart } from "lucide-react";
|
||||
import { Heart, LoaderCircle } from "lucide-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { getCookie } from "@/helpers/cookie";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export default function LikeButton({ post }: { post: PostType }) {
|
||||
const [likes, setLikes] = useState<number>(post.likes.length);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [liked, setLiked] = useState<boolean>(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
style={{
|
||||
color: post.hasLiked ? (theme == "dark" ? "#5ed4f7" : "#05b7eb") : "",
|
||||
borderColor: post.hasLiked
|
||||
? theme == "dark"
|
||||
? "#5ed4f744"
|
||||
: "#05b7eb44"
|
||||
: "",
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (loading || liked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? "https://d2jam.com/api/v1/like"
|
||||
|
@ -35,6 +52,7 @@ export default function LikeButton({ post }: { post: PostType }) {
|
|||
);
|
||||
|
||||
if (!response.ok) {
|
||||
setLoading(false);
|
||||
if (response.status == 401) {
|
||||
redirect("/login");
|
||||
} else {
|
||||
|
@ -42,11 +60,36 @@ export default function LikeButton({ post }: { post: PostType }) {
|
|||
return;
|
||||
}
|
||||
} else {
|
||||
setLikes(parseInt(await response.text()));
|
||||
const data = await response.json();
|
||||
setLikes(parseInt(data.likes));
|
||||
post.hasLiked = data.action === "like";
|
||||
setLoading(false);
|
||||
setLiked(data.action === "like");
|
||||
setTimeout(() => setLiked(false), 1000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Heart size={16} /> {likes}
|
||||
{loading ? (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,59 +1,285 @@
|
|||
import { Avatar, Button, Card, CardBody, Spacer } from "@nextui-org/react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownSection,
|
||||
DropdownTrigger,
|
||||
Spacer,
|
||||
} from "@nextui-org/react";
|
||||
import { formatDistance } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { PostType } from "@/types/PostType";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
import {
|
||||
Flag,
|
||||
MessageCircle,
|
||||
Minus,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
ShieldX,
|
||||
Trash,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import LikeButton from "./LikeButton";
|
||||
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({
|
||||
post,
|
||||
style,
|
||||
user,
|
||||
}: {
|
||||
post: PostType;
|
||||
style: PostStyle;
|
||||
user?: UserType;
|
||||
}) {
|
||||
const [minimized, setMinimized] = useState<boolean>(false);
|
||||
const [hidden, setHidden] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Card className="bg-opacity-60">
|
||||
<Card
|
||||
className="bg-opacity-60 !duration-500 ease-in-out !transition-all"
|
||||
style={{ display: hidden ? "none" : "flex" }}
|
||||
>
|
||||
<CardBody className="p-5">
|
||||
{style == "cozy" && (
|
||||
<div>
|
||||
<p className="text-2xl">{post.title}</p>
|
||||
{(style == "cozy" || style == "adaptive") &&
|
||||
(minimized ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<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">
|
||||
<p>By</p>
|
||||
<Link
|
||||
href={`/u/${post.author.slug}`}
|
||||
className="flex items-center gap-2"
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onPress={() => setMinimized(false)}
|
||||
>
|
||||
<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}
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</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" && (
|
||||
<div>
|
||||
<p className="text-2xl">{post.title}</p>
|
||||
|
@ -68,6 +294,9 @@ export default function PostCard({
|
|||
size="sm"
|
||||
className="w-6 h-6"
|
||||
src={post.author.profilePicture}
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
<p>{post.author.name}</p>
|
||||
</Link>
|
||||
|
@ -93,6 +322,9 @@ export default function PostCard({
|
|||
size="sm"
|
||||
className="w-6 h-6"
|
||||
src={post.author.profilePicture}
|
||||
classNames={{
|
||||
base: "bg-transparent",
|
||||
}}
|
||||
/>
|
||||
<p>{post.author.name}</p>
|
||||
</Link>
|
||||
|
@ -104,44 +336,6 @@ export default function PostCard({
|
|||
</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>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -12,23 +12,59 @@ import {
|
|||
} from "@nextui-org/react";
|
||||
import { PostSort } from "@/types/PostSort";
|
||||
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() {
|
||||
const [posts, setPosts] = useState<PostType[]>();
|
||||
const [sort, setSort] = useState<PostSort>("newest");
|
||||
const [style, setStyle] = useState<PostStyle>("cozy");
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
const response = await fetch(
|
||||
const loadUserAndPosts = async () => {
|
||||
setLoading(true);
|
||||
// Fetch the user
|
||||
const userResponse = await fetch(
|
||||
process.env.NEXT_PUBLIC_MODE === "PROD"
|
||||
? `https://d2jam.com/api/v1/posts?sort=${sort}`
|
||||
: `http://localhost:3005/api/v1/posts?sort=${sort}`
|
||||
? `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",
|
||||
}
|
||||
);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPosts();
|
||||
loadUserAndPosts();
|
||||
}, [sort]);
|
||||
|
||||
return (
|
||||
|
@ -37,7 +73,11 @@ export default function Posts() {
|
|||
<div className="flex gap-2">
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button size="sm" className="text-xs" variant="faded">
|
||||
<Button
|
||||
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)}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
|
@ -45,21 +85,32 @@ export default function Posts() {
|
|||
onAction={(key) => {
|
||||
setSort(key as PostSort);
|
||||
}}
|
||||
className="text-black"
|
||||
className="text-[#333] dark:text-white"
|
||||
>
|
||||
<DropdownItem key="newest">Newest</DropdownItem>
|
||||
<DropdownItem key="top">Top</DropdownItem>
|
||||
<DropdownItem key="oldest">Oldest</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button size="sm" className="text-xs" variant="faded">
|
||||
<Button
|
||||
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
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button size="sm" className="text-xs" variant="faded">
|
||||
<Button
|
||||
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)}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
|
@ -67,7 +118,7 @@ export default function Posts() {
|
|||
onAction={(key) => {
|
||||
setStyle(key as PostStyle);
|
||||
}}
|
||||
className="text-black"
|
||||
className="text-[#333] dark:text-white"
|
||||
>
|
||||
<DropdownItem key="cozy">Cozy</DropdownItem>
|
||||
<DropdownItem key="compact">Compact</DropdownItem>
|
||||
|
@ -77,14 +128,22 @@ export default function Posts() {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 p-4">
|
||||
{posts &&
|
||||
posts.map((post) => (
|
||||
<div key={post.id}>
|
||||
<PostCard post={post} style={style} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center p-6">
|
||||
<LoaderCircle
|
||||
className="animate-spin text-[#333] dark:text-[#999]"
|
||||
size={24}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
return <div></div>;
|
||||
|
|
42
src/components/theme-toggle/index.tsx
Normal file
42
src/components/theme-toggle/index.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { MoonIcon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [isSpinning, setIsSpinning] = useState<boolean>(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isSpinning) return;
|
||||
|
||||
setIsSpinning(true);
|
||||
setTimeout(() => setIsSpinning(false), 500);
|
||||
|
||||
console.log(theme);
|
||||
setTheme(theme === "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">
|
||||
{theme === "dark" && <MoonIcon />}
|
||||
{theme === "light" && <Sun />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,7 +3,7 @@ import Timer from "./Timer";
|
|||
|
||||
export default function Timers() {
|
||||
return (
|
||||
<div className="text-white">
|
||||
<div className="text-[#333] dark:text-white ease-in-out transition-color duration-500">
|
||||
<Timer
|
||||
name="Jam Start"
|
||||
targetDate={new Date("2025-04-04T18:00:00-05:00")}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { UserType } from "./UserType";
|
||||
|
||||
export interface PostType {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
author: {
|
||||
slug: string;
|
||||
profilePicture: string;
|
||||
name: string;
|
||||
};
|
||||
author: UserType;
|
||||
createdAt: Date;
|
||||
likes: [];
|
||||
hasLiked: boolean;
|
||||
}
|
||||
|
|
|
@ -4,4 +4,6 @@ export interface UserType {
|
|||
name: string;
|
||||
profilePicture: string;
|
||||
createdAt: Date;
|
||||
mod: boolean;
|
||||
admin: boolean;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ const config: Config = {
|
|||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [nextui()],
|
||||
plugins: [nextui(), require("@tailwindcss/typography")],
|
||||
};
|
||||
export default config;
|
||||
|
|
Loading…
Reference in a new issue