Compare commits

..

18 commits

Author SHA1 Message Date
Vitaliy
e0319729c8
Merge 6d857ffc49 into 8002647ec7 2025-01-16 23:11:31 +03:00
Benjamin Barbeau
8002647ec7
Update README.md 2025-01-16 14:56:08 -05:00
Benjamin Barbeau
c74f207d1a
Update README.md 2025-01-16 14:55:47 -05:00
Benjamin Barbeau
9cc4f99e3d
Update README.md 2025-01-16 14:54:39 -05:00
Benjamin Barbeau
c1693a3166
Update README.md 2025-01-16 14:50:50 -05:00
Ategon
4642ea9032 Send to main page after post creation 2025-01-16 04:13:31 -05:00
Ategon
a879690129 Move forum to main page 2025-01-16 03:19:35 -05:00
Ategon
0e35c13292 Add favicon 2025-01-16 02:34:53 -05:00
Ategon
2d2ffa7d12 Fix cookie checking 2025-01-16 02:24:18 -05:00
Ategon
047d630b14 Add post creation 2025-01-16 01:42:36 -05:00
Ategon
2178675f2a Add sign up 2025-01-16 00:16:02 -05:00
Ategon
aaa9305c09 Temp remove notebook pen 2025-01-15 23:11:56 -05:00
Ategon
fc5de7ba12 Temp remove signup 2025-01-15 23:10:38 -05:00
Ategon
3a2d5b1cb3 Fix prod ternaries 2025-01-15 23:09:04 -05:00
Ategon
ad7311247b Temp remove signup 2025-01-15 22:45:03 -05:00
Ategon
51d92b2fc4 Add login functionality 2025-01-15 22:42:11 -05:00
Ategon
e61dd263c5 Add dev backend support 2025-01-15 20:51:28 -05:00
Ategon
2c05a7464f Add dare2jam branding 2025-01-15 20:40:32 -05:00
19 changed files with 881 additions and 81 deletions

View file

@ -2,15 +2,37 @@
Frontend for a game jam site
To run using next.js (for development)
## Things used
```
npm i
npm run dev
```
- Typescript (language)
- Next.js (web framework)
- Tailwind (css framework)
- Lucide (icons)
- Eslint (static code analysis)
- Framer motion (animations)
- React Toastify (toasts)
To run using docker and docker compose
## Running for development
Prerequisites:
- node.js or equivalent
To start up the site locally for development you need to:
1. Go to a spot you want to be the parent folder for where the folder for Jamjar goes (e.g. navigate to it in terminal)
2. Clone the repository aka get a local copy of the files (e.g. by running `git clone https://github.com/Dare2Jam/Jamjar.git`)
3. Go into the folder you just cloned in (e.g. using `cd Jamjar`)
4. Install dependencies needed for the site (e.g. `npm i`)
5. Create a `.env` file in the folder which is used for environment variables. In this you would set NEXT_PUBLIC_MODE to either PROD or DEV depending on what backend data you want to load in (dev loads it from a locally running jamcore, PROD loads it from the production site)
```
docker compose up --build -d
NEXT_PUBLIC_MODE=DEV
```
6. Run the site using `npm run dev` which will start up a dev server that will hot reload as you make changes (most of the time)
7. Go to https://localhost:3000 (or another port if it says it started up the site on a different port)
## Running using docker
Prerequisites:
- docker
If you want to start up the frontend using docker instead of what is above (either for development or for a production site) you can run `docker compose up --build -d` to build the image and then run it in the background. This will need to be done after any changes you make to rebuild the image

25
package-lock.json generated
View file

@ -15,7 +15,8 @@
"lucide-react": "^0.453.0",
"next": "15.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-toastify": "^11.0.3"
},
"devDependencies": {
"@types/node": "^20",
@ -7605,6 +7606,28 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
"integrity": "sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/react-toastify/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -16,7 +16,8 @@
"lucide-react": "^0.453.0",
"next": "15.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-toastify": "^11.0.3"
},
"devDependencies": {
"@types/node": "^20",

BIN
public/images/dare2jam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View file

@ -1,29 +0,0 @@
import Posts from "@/components/posts";
import Timers from "@/components/timers";
import { Image } from "@nextui-org/react";
export default function Home() {
return (
<div>
<div className="absolute left-0 top-0 w-full h-full z-0">
<Image
src="/images/bg.jpg"
alt="Home background"
className="object-cover w-full h-full"
radius="none"
loading="eager"
removeWrapper
/>
<div className="absolute left-0 top-0 w-full h-full bg-gradient-to-r from-black/50 to-transparent z-10" />
</div>
<div className="z-10 relative flex">
<div>
<Posts />
</div>
<div className="w-1/3 flex justify-end">
<Timers />
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,108 @@
"use client";
import { getCookie, hasCookie } from "@/helpers/cookie";
import { Button, Form, Input, Textarea } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { useState } from "react";
import { toast } from "react-toastify";
export default function CreatePostPage() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [errors, setErrors] = useState({});
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"
validationErrors={errors}
onReset={() => {
setTitle("");
setContent("");
}}
onSubmit={async (e) => {
e.preventDefault();
if (!title && !content) {
setErrors({
title: "Please enter a valid title",
content: "Please enter valid content",
});
return;
}
if (!title) {
setErrors({ title: "Please enter a valid title" });
return;
}
if (!content) {
setErrors({ content: "Please enter valid content" });
return;
}
if (!hasCookie("token")) {
setErrors({ content: "You are not logged in" });
return;
}
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({
title: title,
content: content,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie("token")}`,
},
}
);
if (response.status == 401) {
setErrors({ content: "Invalid user" });
return;
}
toast.success("Successfully created post");
redirect("/");
}}
>
<Input
isRequired
label="Title"
labelPlacement="outside"
name="title"
placeholder="Enter a title"
type="text"
value={title}
onValueChange={setTitle}
/>
<Textarea
isRequired
label="Content"
labelPlacement="outside"
name="content"
placeholder="Enter the post body"
value={content}
onValueChange={setContent}
/>
<div className="flex gap-2">
<Button color="primary" type="submit">
Create
</Button>
<Button type="reset" variant="flat">
Reset
</Button>
</div>
</Form>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -3,11 +3,12 @@ import { Inter } from "next/font/google";
import "./globals.css";
import Navbar from "../components/navbar";
import Providers from "./providers";
import { ToastContainer } from "react-toastify";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Edikoyo Jam",
title: "Dare2Jam",
description: "A community built game jam!",
};
@ -24,6 +25,7 @@ export default function RootLayout({
<div className="bg-zinc-100 dark:bg-zinc-950 min-h-screen">
<Navbar />
<div className="max-w-8xl mx-auto">{children}</div>
<ToastContainer />
</div>
</div>
</Providers>

102
src/app/login/page.tsx Normal file
View file

@ -0,0 +1,102 @@
"use client";
import { Button, Form, Input } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { useState } from "react";
import { toast } from "react-toastify";
export default function UserPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errors, setErrors] = useState({});
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"
validationErrors={errors}
onReset={() => {
setUsername("");
setPassword("");
}}
onSubmit={async (e) => {
e.preventDefault();
if (!username && !password) {
setErrors({
username: "Please enter a valid username",
password: "Please enter a valid password",
});
return;
}
if (!username) {
setErrors({ username: "Please enter a valid username" });
return;
}
if (!password) {
setErrors({ password: "Please enter a valid password" });
return;
}
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/login"
: "http://localhost:3005/api/v1/login",
{
body: JSON.stringify({ username: username, password: password }),
method: "POST",
headers: { "Content-Type": "application/json" },
}
);
if (response.status == 401) {
setErrors({ password: "Invalid username or password" });
setPassword("");
return;
}
const { token, user } = await response.json();
document.cookie = `token=${token}`;
document.cookie = `user=${user.slug}`;
toast.success("Successfully logged in");
redirect("/");
}}
>
<Input
isRequired
label="Username"
labelPlacement="outside"
name="username"
placeholder="Enter your username"
type="text"
value={username}
onValueChange={setUsername}
/>
<Input
isRequired
label="Password"
labelPlacement="outside"
name="password"
placeholder="Enter your password"
type="password"
value={password}
onValueChange={setPassword}
/>
<div className="flex gap-2">
<Button color="primary" type="submit">
Submit
</Button>
<Button type="reset" variant="flat">
Reset
</Button>
</div>
</Form>
</div>
);
}

21
src/app/logout/page.tsx Normal file
View file

@ -0,0 +1,21 @@
"use client";
import { redirect } from "next/navigation";
import React, { useEffect } from "react";
import { toast } from "react-toastify";
export default function UserPage() {
useEffect(() => {
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
toast.success("Successfully logged out");
redirect("/");
});
return (
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<p>Logging out...</p>
</div>
);
}

View file

@ -1,10 +1,53 @@
import { Image } from "@nextui-org/image";
import { Button } from "@nextui-org/button";
import { SiDiscord } from "@icons-pack/react-simple-icons";
import { Link } from "@nextui-org/react";
import Posts from "@/components/posts";
import Timers from "@/components/timers";
export default async function Home() {
return (
<div className="w-full">
<div className="fixed left-0 top-0 w-full h-full z-0">
<Image
src="/images/bg.jpg"
alt="Home background"
className="object-cover w-full h-full"
radius="none"
loading="eager"
removeWrapper
/>
<div className="absolute left-0 top-0 w-full h-full bg-gradient-to-r from-black/50 to-transparent z-10" />
</div>
<div className="z-10 relative flex w-full flex-wrap">
<div>
<div className="flex flex-col gap-4 py-4 sm:py-8 md:py-12 pl-16">
<h1 className="text-3xl sm:text-4xl md:text-5xl">Dare2Jam</h1>
<p className="text-lg sm:text-xl">April 4th - 7th</p>
<div className="flex gap-2">
<Link href="https://discord.gg/rfmKzM6ASw" target="_blank">
<Button
variant="bordered"
className="border-white/50 text-white"
startContent={<SiDiscord />}
>
Join the Discord
</Button>
</Link>
</div>
</div>
<Posts />
</div>
<div className="w-1/3 flex justify-end py-4 sm:py-8 md:py-12 flex-grow">
<Timers />
</div>
</div>
</div>
);
}
{
/* <div>
<div className="absolute left-0 top-0 w-full h-full z-0">
<Image
src="/images/bg.jpg"
@ -19,14 +62,10 @@ export default async function Home() {
<div className="relative left-0 top-0 z-10 px-8">
<div className="flex gap-20">
<div className="flex flex-col gap-4 py-16 sm:py-36 md:py-72">
<h1 className="text-3xl sm:text-4xl md:text-5xl">Edikoyo Jam</h1>
<h1 className="text-3xl sm:text-4xl md:text-5xl">Dare2Jam</h1>
<p className="text-lg sm:text-xl">April 4th - 7th</p>
<div className="flex gap-2">
<a
href="https://discord.gg/rfmKzM6ASw"
target="_blank"
rel="noopener noreferrer"
>
<Link href="https://discord.gg/rfmKzM6ASw" target="_blank">
<Button
variant="bordered"
className="border-white/50 text-white"
@ -34,11 +73,10 @@ export default async function Home() {
>
Join the Discord
</Button>
</a>
</Link>
</div>
</div>
</div>
</div>
</div>
);
</div> */
}

137
src/app/signup/page.tsx Normal file
View file

@ -0,0 +1,137 @@
"use client";
import { Button, Form, Input } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { useState } from "react";
import { toast } from "react-toastify";
export default function UserPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [password2, setPassword2] = useState("");
const [errors, setErrors] = useState({});
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"
validationErrors={errors}
onReset={() => {
setUsername("");
setPassword("");
setPassword2("");
}}
onSubmit={async (e) => {
e.preventDefault();
if (!password || !username || !password2) {
const localErrors: Record<string, string> = {};
if (password && password.length < 8) {
setPassword2("");
setErrors({ password: "Password must be minimum 8 characters" });
return;
}
if (!password) {
localErrors["password"] = "Please enter a valid password";
}
if (!password2) {
localErrors["password2"] = "Please reenter your password";
}
if (!username) {
localErrors["username"] = "Please enter a valid username";
}
setErrors(localErrors);
return;
}
if (username.length > 32) {
setPassword2("");
setErrors({ password: "Usernames can be maximum 32 characters" });
return;
}
if (password.length < 8) {
setPassword2("");
setErrors({ password: "Password must be minimum 8 characters" });
return;
}
if (password != password2) {
setPassword2("");
setErrors({ password2: "Passwords do not match" });
return;
}
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/signup"
: "http://localhost:3005/api/v1/signup",
{
body: JSON.stringify({ username: username, password: password }),
method: "POST",
headers: { "Content-Type": "application/json" },
}
);
if (response.status == 409) {
setErrors({ username: "User already exists" });
setPassword2("");
return;
}
const { token, user } = await response.json();
document.cookie = `token=${token}`;
document.cookie = `user=${user.slug}`;
toast.success("Successfully signed up");
redirect("/");
}}
>
<Input
isRequired
label="Username"
labelPlacement="outside"
name="username"
placeholder="Enter your username"
type="text"
value={username}
onValueChange={setUsername}
/>
<Input
isRequired
label="Password"
labelPlacement="outside"
name="password"
placeholder="Enter your password"
type="password"
value={password}
onValueChange={setPassword}
/>
<Input
isRequired
label="Password Confirmation"
labelPlacement="outside"
name="password2"
placeholder="Reenter your password"
type="password"
value={password2}
onValueChange={setPassword2}
/>
<div className="flex gap-2">
<Button color="primary" type="submit">
Submit
</Button>
<Button type="reset" variant="flat">
Reset
</Button>
</div>
</Form>
</div>
);
}

View file

@ -1,3 +1,5 @@
"use client";
import {
Navbar as NavbarBase,
NavbarBrand,
@ -6,10 +8,63 @@ import {
} from "@nextui-org/navbar";
import { Link } from "@nextui-org/link";
import { Divider } from "@nextui-org/divider";
import { Image, Tooltip } from "@nextui-org/react";
import {
Avatar,
Button,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
Image,
Spacer,
Tooltip,
} from "@nextui-org/react";
import { SiDiscord, SiForgejo, SiGithub } from "@icons-pack/react-simple-icons";
import { LogInIcon, Menu, NotebookPen, SquarePen } from "lucide-react";
import { useEffect, useState } from "react";
import { hasCookie, getCookie } from "@/helpers/cookie";
import { usePathname } from "next/navigation";
import { UserType } from "@/types/UserType";
export default function Navbar() {
const [user, setUser] = useState<UserType>();
const pathname = usePathname();
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
loadUser();
async function loadUser() {
if (!hasCookie("token")) {
setUser(undefined);
return;
}
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/self?username=${getCookie("user")}`
: `http://localhost:3005/api/v1/self?username=${getCookie("user")}`,
{
headers: { authorization: `Bearer ${getCookie("token")}` },
}
);
if (response.status == 200) {
setUser(await response.json());
} else {
setUser(undefined);
}
}
}, [pathname]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<NavbarBase
shouldHideOnScroll
@ -21,20 +76,92 @@ export default function Navbar() {
href="/"
className="duration-500 ease-in-out transition-all transform hover:scale-110"
>
<Image src="/images/edikoyo.png" alt="Edikoyo logo" width={160} />
<Image src="/images/dare2jam.png" alt="Dare2Jam logo" width={80} />
</Link>
</NavbarBrand>
<NavbarContent>
<NavbarItem>
<Link
href="/app"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
>
Beta Site
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
{isMobile ? (
user ? (
<Dropdown>
<DropdownTrigger>
<Avatar src={user.profilePicture} />
</DropdownTrigger>
<DropdownMenu className="text-black">
<DropdownItem key="create-post" href="/create-post">
Create Post
</DropdownItem>
<DropdownItem
key="github"
href="https://github.com/Ategon/Jamjar"
>
GitHub
</DropdownItem>
<DropdownItem
key="forgejo"
href="https://git.edikoyo.com/Ategon/Jamjar"
>
Forgejo
</DropdownItem>
<DropdownItem
key="discord"
href="https://discord.gg/rfmKzM6ASw"
>
Discord
</DropdownItem>
<DropdownItem key="logout" color="danger" href="/logout">
Logout
</DropdownItem>
</DropdownMenu>
</Dropdown>
) : (
<Dropdown>
<DropdownTrigger>
<Menu />
</DropdownTrigger>
<DropdownMenu className="text-black">
<DropdownItem
key="github"
href="https://github.com/Ategon/Jamjar"
>
GitHub
</DropdownItem>
<DropdownItem
key="forgejo"
href="https://git.edikoyo.com/Ategon/Jamjar"
>
Forgejo
</DropdownItem>
<DropdownItem
key="discord"
href="https://discord.gg/rfmKzM6ASw"
>
Discord
</DropdownItem>
<DropdownItem key="login" href="/login">
Log In
</DropdownItem>
<DropdownItem key="signup" href="/signup">
Sign Up
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
) : (
<div className="flex gap-3 items-center">
{user && (
<NavbarItem>
<Link href="/create-post">
<Button
endContent={<SquarePen />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
variant="bordered"
>
Create Post
</Button>
</Link>
<Spacer x={32} />
</NavbarItem>
)}
<NavbarItem>
<Tooltip
delay={1000}
@ -83,7 +210,10 @@ export default function Navbar() {
</Link>
</NavbarItem>
<Divider orientation="vertical" className="h-1/2" />
{/* <NavbarItem>
{!user ? (
<div className="flex gap-3 items-center">
<NavbarItem>
<Link href="/login">
<Button
endContent={<LogInIcon />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
@ -91,8 +221,209 @@ export default function Navbar() {
>
Log In
</Button>
</NavbarItem> */}
</Link>
</NavbarItem>
<NavbarItem>
<Link href="/signup">
<Button
endContent={<NotebookPen />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
variant="bordered"
>
Sign up
</Button>
</Link>
</NavbarItem>
</div>
) : (
<Dropdown>
<DropdownTrigger>
<Avatar src={user.profilePicture} />
</DropdownTrigger>
<DropdownMenu>
{/* <DropdownItem
key="profile"
className="text-black"
href="/profile"
>
Profile
</DropdownItem>
<DropdownItem
showDivider
key="settings"
className="text-black"
href="/settings"
>
Settings
</DropdownItem> */}
<DropdownItem
key="logout"
color="danger"
className="text-danger"
href="/logout"
>
Logout
</DropdownItem>
</DropdownMenu>
</Dropdown>
)}
</div>
)}
</NavbarContent>
</NavbarBase>
);
}
/*
{isMobile ? (
// Mobile view
user ? (
<Dropdown>
<DropdownTrigger>
<Avatar src={user.profilePicture} />
</DropdownTrigger>
<DropdownMenu>
<DropdownItem key="create-post" href="/create-post">
Create Post
</DropdownItem>
<DropdownItem key="logout" color="danger" href="/logout">
Logout
</DropdownItem>
</DropdownMenu>
</Dropdown>
) : (
<Dropdown>
<DropdownTrigger>
<Button auto flat className="text-white">
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem key="github" href="https://github.com/Ategon/Jamjar" isExternal>
GitHub
</DropdownItem>
<DropdownItem key="forgejo" href="https://git.edikoyo.com/Ategon/Jamjar" isExternal>
Forgejo
</DropdownItem>
<DropdownItem key="discord" href="https://discord.gg/rfmKzM6ASw" isExternal>
Discord
</DropdownItem>
<DropdownItem key="login" href="/login">
Log In
</DropdownItem>
<DropdownItem key="signup" href="/signup">
Sign Up
</DropdownItem>
</DropdownMenu>
</Dropdown>
)
) : (
user && (
<NavbarItem>
<Link href="/create-post">
<Button
endContent={<SquarePen />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
variant="bordered"
>
Create Post
</Button>
</Link>
<Spacer x={32} />
</NavbarItem>
)
<NavbarItem>
<Tooltip
delay={1000}
content={
<div className="px-1 py-2 text-black text-center">
<div className="text-small font-bold">GitHub</div>
<div className="text-tiny">Source Code</div>
</div>
}
>
<Link
href="https://github.com/Ategon/Jamjar"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
isExternal
>
<SiGithub title="" />
</Link>
</Tooltip>
</NavbarItem>
<NavbarItem>
<Tooltip
delay={1000}
content={
<div className="px-1 py-2 text-black text-center">
<div className="text-small font-bold">Forgejo</div>
<div className="text-tiny">Source Code</div>
</div>
}
>
<Link
href="https://git.edikoyo.com/Ategon/Jamjar"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-red-100"
isExternal
>
<SiForgejo title="" />
</Link>
</Tooltip>
</NavbarItem>
<NavbarItem>
<Link
href="https://discord.gg/rfmKzM6ASw"
className="text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 hover:text-indigo-100"
isExternal
>
<SiDiscord />
</Link>
</NavbarItem>
<Divider orientation="vertical" className="h-1/2" />
{!user ? (
<div className="flex gap-3 items-center">
<NavbarItem>
<Link href="/login">
<Button
endContent={<LogInIcon />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
variant="bordered"
>
Log In
</Button>
</Link>
</NavbarItem>
<NavbarItem>
<Link href="/signup">
<Button
endContent={<NotebookPen />}
className="text-white border-white/50 hover:border-green-100/50 hover:text-green-100 hover:scale-110 transition-all transform duration-500 ease-in-out"
variant="bordered"
>
Sign up
</Button>
</Link>
</NavbarItem>
</div>
) : (
<Dropdown>
<DropdownTrigger>
<Avatar src={user.profilePicture} />
</DropdownTrigger>
<DropdownMenu>
<DropdownItem
key="logout"
color="danger"
className="text-danger"
href="/logout"
>
Logout
</DropdownItem>
</DropdownMenu>
</Dropdown>
)}
)
</NavbarContent> */

View file

@ -1,5 +1,4 @@
import { Avatar, Button, Card, CardBody, Spacer } from "@nextui-org/react";
import { Heart, MessageCircle } from "lucide-react";
import { Avatar, Card, CardBody, Spacer } from "@nextui-org/react";
import { formatDistance } from "date-fns";
import Link from "next/link";
import { PostType } from "@/types/PostType";
@ -36,14 +35,14 @@ export default function PostCard({ post }: { post: PostType }) {
<Spacer y={4} />
<div className="flex gap-3">
{/* <div className="flex gap-3">
<Button size="sm">
<Heart size={16} /> {post.likers.length}
</Button>
<Button size="sm">
<MessageCircle size={16} /> {0}
</Button>
</div>
</div> */}
</CardBody>
</Card>
);

View file

@ -9,7 +9,11 @@ export default function Posts() {
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch("https://jam.edikoyo.com/api/v1/posts");
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/posts"
: "http://localhost:3005/api/v1/posts"
);
setPosts(await response.json());
};

View file

@ -1,3 +1,4 @@
import { Spacer } from "@nextui-org/react";
import Timer from "./Timer";
export default function Timers() {
@ -7,6 +8,8 @@ export default function Timers() {
name="Jam Start"
targetDate={new Date("2025-04-04T18:00:00-05:00")}
/>
<Spacer y={8} />
<p>Site under construction</p>
</div>
);
}

31
src/helpers/cookie.ts Normal file
View file

@ -0,0 +1,31 @@
export function getCookies() {
const pairs = document.cookie.split(";");
const cookies: Record<string, string> = {};
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split("=");
cookies[(pair[0] + "").trim()] = unescape(pair.slice(1).join("="));
}
return cookies;
}
export function getCookie(cookie: string) {
const pairs = document.cookie.split(";");
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].trim().split("=");
if (pair[0] == cookie) {
return pair[1];
}
}
return null;
}
export function hasCookie(cookie: string) {
const pairs = document.cookie.split(";");
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].trim().split("=");
if (pair[0] == cookie) {
return true;
}
}
return false;
}

7
src/types/UserType.ts Normal file
View file

@ -0,0 +1,7 @@
export interface UserType {
id: number;
slug: string;
name: string;
profilePicture: string;
createdAt: Date;
}