Compare commits

..

No commits in common. "aefb9e2018e7b8c1d0ee12bc5a389ac1e2315a4a" and "f26af125b8706bcb36d701926eb28e072457e2e0" have entirely different histories.

25 changed files with 114 additions and 469 deletions

View file

@ -1,17 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { /* config options here */
remotePatterns: [
{
protocol: "https",
hostname: "static-cdn.jtvnw.net",
port: "",
pathname: "/**",
search: "",
},
],
},
}; };
export default nextConfig; export default nextConfig;

Binary file not shown.

Before

(image error) Size: 24 KiB

Binary file not shown.

After

(image error) Size: 24 KiB

Binary file not shown.

Before

Width: 48px  |  Height: 48px  |  Size: 15 KiB

After

Width: 48px  |  Height: 48px  |  Size: 15 KiB

View file

@ -60,6 +60,7 @@ export default function UserPage() {
const { user } = await response.json(); const { user } = await response.json();
const token = response.headers.get("Authorization"); const token = response.headers.get("Authorization");
console.log(response.headers);
if (!token) { if (!token) {
toast.error("Failed to retreive access token"); toast.error("Failed to retreive access token");
@ -104,7 +105,7 @@ export default function UserPage() {
Reset Reset
</Button> </Button>
</div> </div>
<p className="text-[#333] dark:text-white transition-color duration-250"> <p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
Don&apos;t have an account? <Link href="/signup">Sign up</Link> Don&apos;t have an account? <Link href="/signup">Sign up</Link>
</p> </p>
</Form> </Form>

View file

@ -6,28 +6,11 @@ import { toast } from "react-toastify";
export default function UserPage() { export default function UserPage() {
useEffect(() => { useEffect(() => {
async function logout() { document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/logout"
: "http://localhost:3005/api/v1/logout",
{ method: "POST", credentials: "include" }
);
if (response.ok) { toast.success("Successfully logged out");
document.cookie =
"token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
document.cookie =
"user=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
toast.success("Successfully logged out"); redirect("/");
redirect("/");
} else {
toast.error("Error while trying to log out");
}
}
logout();
}); });
return ( return (

View file

@ -1,9 +1,7 @@
"use client"; "use client";
import { Button, Form, Input, Link } from "@nextui-org/react"; import { Button, Form, Input, Link } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-toastify";
export default function UserPage() { export default function UserPage() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -74,7 +72,6 @@ export default function UserPage() {
body: JSON.stringify({ username: username, password: password }), body: JSON.stringify({ username: username, password: password }),
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include",
} }
); );
@ -84,13 +81,13 @@ export default function UserPage() {
return; return;
} }
const { token, user } = await response.json(); // const { token, user } = await response.json();
document.cookie = `token=${token}`; // document.cookie = `token=${token}`;
document.cookie = `user=${user.slug}`; // document.cookie = `user=${user.slug}`;
toast.success("Successfully signed up"); // toast.success("Successfully signed up");
redirect("/"); // redirect("/");
}} }}
> >
<Input <Input
@ -132,7 +129,7 @@ export default function UserPage() {
Reset Reset
</Button> </Button>
</div> </div>
<p className="text-[#333] dark:text-white transition-color duration-250"> <p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
Already have an account? <Link href="/login">Log In</Link> Already have an account? <Link href="/login">Log In</Link>
</p> </p>
</Form> </Form>

View file

@ -97,7 +97,7 @@ export default function Editor({ content, setContent }: EditorProps) {
editorProps: { editorProps: {
attributes: { attributes: {
class: 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 !duration-250 !ease-linear !transition-all", "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",
}, },
}, },
}); });
@ -115,7 +115,7 @@ export default function Editor({ content, setContent }: EditorProps) {
: editor.storage.characterCount.characters() > limit / 2 : editor.storage.characterCount.characters() > limit / 2
? "text-yellow-500" ? "text-yellow-500"
: "text-[#888] dark:text-[#555]" : "text-[#888] dark:text-[#555]"
} transform-color duration-250 ease-linear flex items-center gap-3`} } transform-color duration-500 ease-in-out flex items-center gap-3`}
> >
<svg width="30" height="30" viewBox="0 0 36 36"> <svg width="30" height="30" viewBox="0 0 36 36">
<circle <circle
@ -125,7 +125,7 @@ export default function Editor({ content, setContent }: EditorProps) {
fill="none" fill="none"
stroke={theme === "dark" ? "#333" : "#eee"} stroke={theme === "dark" ? "#333" : "#eee"}
strokeWidth="3" strokeWidth="3"
className="!duration-250 !ease-linear !transition-all" className="transform-all duration-500 ease-in-out"
/> />
<circle <circle
id="progress-circle" id="progress-circle"
@ -146,7 +146,7 @@ export default function Editor({ content, setContent }: EditorProps) {
(1 - editor.storage.characterCount.characters() / limit) * 100 (1 - editor.storage.characterCount.characters() / limit) * 100
} }
transform="rotate(-90 18 18)" transform="rotate(-90 18 18)"
className="!duration-250 !ease-linear !transition-all" className="transform-all duration-500 ease-in-out"
/> />
</svg> </svg>
{editor.storage.characterCount.characters()} / {limit} characters {editor.storage.characterCount.characters()} / {limit} characters

View file

@ -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-250"> <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-250"> <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 /> <Calendar />
<p>Dare2Jam 1</p> <p>Dare2Jam 1</p>
</div> </div>

View file

@ -1,7 +1,5 @@
"use client";
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode } from "react";
interface ButtonActionProps { interface ButtonActionProps {
icon?: ReactNode; icon?: ReactNode;
@ -14,28 +12,10 @@ export default function ButtonAction({
onPress, onPress,
name, name,
}: ButtonActionProps) { }: ButtonActionProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return ( return (
<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-[#333] dark:text-white border-[#333]/50 dark:border-white/50 hover:scale-110 transition-all transform duration-500 ease-in-out"
!reduceMotion ? "hover:scale-110" : ""
}`}
variant="bordered" variant="bordered"
onPress={onPress} onPress={onPress}
> >

View file

@ -1,7 +1,5 @@
"use client";
import { Button, Link } from "@nextui-org/react"; import { Button, Link } from "@nextui-org/react";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode } from "react";
interface ButtonLinkProps { interface ButtonLinkProps {
icon?: ReactNode; icon?: ReactNode;
@ -10,34 +8,14 @@ interface ButtonLinkProps {
} }
export default function ButtonLink({ icon, href, name }: ButtonLinkProps) { export default function ButtonLink({ icon, href, name }: ButtonLinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return ( return (
<Link <Link
href={href} href={href}
className={`flex justify-center duration-500 ease-in-out transition-all transform ${ className="flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
!reduceMotion ? "hover:scale-110" : ""
}`}
> >
<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-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out"
!reduceMotion ? "hover:scale-110" : ""
}`}
variant="bordered" variant="bordered"
> >
{name} {name}

View file

@ -1,7 +1,5 @@
"use client";
import { Link } from "@nextui-org/react"; import { Link } from "@nextui-org/react";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode } from "react";
interface IconLinkProps { interface IconLinkProps {
icon: ReactNode; icon: ReactNode;
@ -9,28 +7,10 @@ interface IconLinkProps {
} }
export default function IconLink({ icon, href }: IconLinkProps) { export default function IconLink({ icon, href }: IconLinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return ( return (
<Link <Link
href={href} href={href}
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${ 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"
!reduceMotion ? "hover:scale-125" : ""
} transition-color`}
> >
{icon} {icon}
</Link> </Link>

View file

@ -1,7 +1,4 @@
"use client";
import { Link as BaseLink } from "@nextui-org/react"; import { Link as BaseLink } from "@nextui-org/react";
import { useEffect, useState } from "react";
interface LinkProps { interface LinkProps {
name: string; name: string;
@ -9,28 +6,10 @@ interface LinkProps {
} }
export default function Link({ name, href }: LinkProps) { export default function Link({ name, href }: LinkProps) {
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return ( return (
<BaseLink <BaseLink
href={href} href={href}
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${ 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"
!reduceMotion ? "hover:scale-110" : ""
} transition-color`}
> >
{name} {name}
</BaseLink> </BaseLink>

View file

@ -75,7 +75,7 @@ export default function MobileNavbar() {
> >
<Image <Image
as={NextImage} as={NextImage}
src="/images/D2J_Icon.png" src="/images/hereyougopomo.png"
className="min-w-[70px]" className="min-w-[70px]"
alt="Dare2Jam logo" alt="Dare2Jam logo"
width={70} width={70}

View file

@ -38,21 +38,6 @@ export default function PCNavbar() {
const [jam, setJam] = useState<JamType | null>(); const [jam, setJam] = useState<JamType | null>();
const [isInJam, setIsInJam] = useState<boolean>(); const [isInJam, setIsInJam] = useState<boolean>();
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
@ -106,13 +91,11 @@ export default function PCNavbar() {
<NavbarBrand className="flex-grow-0"> <NavbarBrand className="flex-grow-0">
<Link <Link
href="/" href="/"
className={`duration-500 ease-in-out transition-all transform ${ className="duration-500 ease-in-out transition-all transform hover:scale-110"
reduceMotion ? "" : "hover:scale-110"
}`}
> >
<Image <Image
as={NextImage} as={NextImage}
src="/images/D2J_Icon.png" src="/images/hereyougopomo.png"
className="min-w-[70px]" className="min-w-[70px]"
alt="Dare2Jam logo" alt="Dare2Jam logo"
width={70} width={70}

View file

@ -15,7 +15,7 @@ interface NavbarUserProps {
export default function PCNavbarUser({ user }: NavbarUserProps) { export default function PCNavbarUser({ user }: NavbarUserProps) {
return ( return (
<NavbarItem> <NavbarItem>
<Dropdown backdrop="opaque"> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Avatar <Avatar
src={user.profilePicture} src={user.profilePicture}
@ -28,7 +28,7 @@ export default function PCNavbarUser({ user }: NavbarUserProps) {
<DropdownMenu> <DropdownMenu>
<DropdownItem <DropdownItem
key="profile" key="profile"
className="text-[#333] dark:text-white" className="text-black"
href={`/u/${user.slug}`} href={`/u/${user.slug}`}
> >
Profile Profile
@ -36,7 +36,7 @@ export default function PCNavbarUser({ user }: NavbarUserProps) {
<DropdownItem <DropdownItem
showDivider showDivider
key="settings" key="settings"
className="text-[#333] dark:text-white" className="text-black"
href="/settings" href="/settings"
> >
Settings Settings

View file

@ -6,7 +6,7 @@ import { Heart, LoaderCircle } 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, useEffect } from "react"; import { useState } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
export default function LikeButton({ post }: { post: PostType }) { export default function LikeButton({ post }: { post: PostType }) {
@ -14,21 +14,6 @@ export default function LikeButton({ post }: { post: PostType }) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [liked, setLiked] = useState<boolean>(false); const [liked, setLiked] = useState<boolean>(false);
const { theme } = useTheme(); const { theme } = useTheme();
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
return ( return (
<Button <Button
@ -71,7 +56,7 @@ export default function LikeButton({ post }: { post: PostType }) {
if (response.status == 401) { if (response.status == 401) {
redirect("/login"); redirect("/login");
} else { } else {
toast.error("An error occurred"); toast.error("An error occured");
return; return;
} }
} else { } else {
@ -94,11 +79,7 @@ export default function LikeButton({ post }: { post: PostType }) {
<Heart size={16} /> <Heart size={16} />
<Heart <Heart
size={16} size={16}
className={ className={liked ? "animate-ping absolute top-0 left-0" : ""}
liked && !reduceMotion
? "animate-ping absolute top-0 left-0"
: "absolute top-0 left-0"
}
style={{ style={{
position: "absolute", position: "absolute",
top: "0", top: "0",

View file

@ -46,7 +46,7 @@ export default function PostCard({
return ( return (
<Card <Card
className="bg-opacity-60 !duration-250 !ease-linear !transition-all" className="bg-opacity-60 !duration-500 ease-in-out !transition-all"
style={{ display: hidden ? "none" : "flex" }} style={{ display: hidden ? "none" : "flex" }}
> >
<CardBody className="p-5"> <CardBody className="p-5">
@ -128,7 +128,7 @@ export default function PostCard({
<Spacer y={4} /> <Spacer y={4} />
<div <div
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all" className="prose dark:prose-invert transition-all !duration-500 ease-in-out"
dangerouslySetInnerHTML={{ __html: post.content }} dangerouslySetInnerHTML={{ __html: post.content }}
/> />
@ -145,7 +145,7 @@ export default function PostCard({
> >
<MessageCircle size={16} /> {0} <MessageCircle size={16} /> {0}
</Button> </Button>
<Dropdown backdrop="opaque"> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button size="sm" variant="bordered" isIconOnly> <Button size="sm" variant="bordered" isIconOnly>
<MoreVertical size={16} /> <MoreVertical size={16} />
@ -201,8 +201,8 @@ export default function PostCard({
<></> <></>
)} )}
</DropdownSection> </DropdownSection>
{user?.mod ? ( <DropdownSection title="Mod Zone">
<DropdownSection title="Mod Zone"> {user?.mod ? (
<DropdownItem <DropdownItem
key="remove" key="remove"
startContent={<X />} startContent={<X />}
@ -236,45 +236,45 @@ export default function PostCard({
> >
Remove Remove
</DropdownItem> </DropdownItem>
{user?.admin && !post.author.mod ? ( ) : (
<DropdownItem <></>
key="promote-mod" )}
startContent={<Shield />} {user?.admin && !post.author.mod ? (
description="Promote user to Mod" <DropdownItem
> key="promote-mod"
Appoint as mod startContent={<Shield />}
</DropdownItem> description="Promote user to Mod"
) : ( >
<></> Appoint as mod
)} </DropdownItem>
{user?.admin && ) : (
post.author.mod && <></>
post.author.id !== user.id ? ( )}
<DropdownItem {user?.admin &&
key="demote-mod" post.author.mod &&
startContent={<ShieldX />} post.author.id !== user.id ? (
description="Demote user from Mod" <DropdownItem
> key="demote-mod"
Remove as mod startContent={<ShieldX />}
</DropdownItem> description="Demote user from Mod"
) : ( >
<></> Remove as mod
)} </DropdownItem>
{user?.admin && !post.author.admin ? ( ) : (
<DropdownItem <></>
key="promote-admin" )}
startContent={<ShieldAlert />} {user?.admin && !post.author.admin ? (
description="Promote user to Admin" <DropdownItem
> key="promote-admin"
Appoint as admin startContent={<ShieldAlert />}
</DropdownItem> description="Promote user to Admin"
) : ( >
<></> Appoint as admin
)} </DropdownItem>
</DropdownSection> ) : (
) : ( <></>
<></> )}
)} </DropdownSection>
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</div> </div>

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { ReactNode, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PostCard from "./PostCard"; import PostCard from "./PostCard";
import { PostType } from "@/types/PostType"; import { PostType } from "@/types/PostType";
import { import {
@ -14,31 +14,12 @@ import { PostSort } from "@/types/PostSort";
import { PostStyle } from "@/types/PostStyle"; import { PostStyle } from "@/types/PostStyle";
import { getCookie } from "@/helpers/cookie"; import { getCookie } from "@/helpers/cookie";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { import { LoaderCircle } from "lucide-react";
Calendar,
Calendar1,
CalendarArrowDown,
CalendarCog,
CalendarDays,
CalendarFold,
CalendarRange,
Clock1,
Clock2,
Clock3,
Clock4,
ClockArrowDown,
ClockArrowUp,
LoaderCircle,
Sparkles,
Trophy,
} from "lucide-react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { PostTime } from "@/types/PostTimes";
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 [time, setTime] = useState<PostTime>("all");
const [style, setStyle] = useState<PostStyle>("cozy"); const [style, setStyle] = useState<PostStyle>("cozy");
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
@ -64,8 +45,8 @@ export default function Posts() {
// Fetch posts with userSlug if user is available // Fetch posts with userSlug if user is available
const postsResponse = await fetch( const postsResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}` ? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${userData.slug}`
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}` : `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}`
); );
setPosts(await postsResponse.json()); setPosts(await postsResponse.json());
setLoading(false); setLoading(false);
@ -75,8 +56,8 @@ export default function Posts() {
// Fetch posts without userSlug if user is not available // Fetch posts without userSlug if user is not available
const postsResponse = await fetch( const postsResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}` ? `https://d2jam.com/api/v1/posts?sort=${sort}`
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}` : `http://localhost:3005/api/v1/posts?sort=${sort}`
); );
setPosts(await postsResponse.json()); setPosts(await postsResponse.json());
setLoading(false); setLoading(false);
@ -84,107 +65,20 @@ export default function Posts() {
}; };
loadUserAndPosts(); loadUserAndPosts();
}, [sort, time]); }, [sort]);
const sorts: Record<
PostSort,
{ name: string; icon: ReactNode; description: string }
> = {
top: {
name: "Top",
icon: <Trophy />,
description: "Shows the most liked posts first",
},
newest: {
name: "Newest",
icon: <ClockArrowUp />,
description: "Shows the newest posts first",
},
oldest: {
name: "Oldest",
icon: <ClockArrowDown />,
description: "Shows the oldest posts first",
},
};
const times: Record<
PostTime,
{ name: string; icon: ReactNode; description: string }
> = {
hour: {
name: "Hour",
icon: <Clock1 />,
description: "Shows posts from the last hour",
},
three_hours: {
name: "Three Hours",
icon: <Clock2 />,
description: "Shows posts from the last three hours",
},
six_hours: {
name: "Six Hours",
icon: <Clock3 />,
description: "Shows posts from the last six hours",
},
twelve_hours: {
name: "Twelve Hours",
icon: <Clock4 />,
description: "Shows posts from the last twelve hours",
},
day: {
name: "Day",
icon: <Calendar />,
description: "Shows posts from the last day",
},
week: {
name: "Week",
icon: <CalendarDays />,
description: "Shows posts from the last week",
},
month: {
name: "Month",
icon: <CalendarRange />,
description: "Shows posts from the last month",
},
three_months: {
name: "Three Months",
icon: <CalendarFold />,
description: "Shows posts from the last three months",
},
six_months: {
name: "Six Months",
icon: <CalendarCog />,
description: "Shows posts from the last six months",
},
nine_months: {
name: "Nine Months",
icon: <CalendarArrowDown />,
description: "Shows posts from the last nine months",
},
year: {
name: "Year",
icon: <Calendar1 />,
description: "Shows posts from the last year",
},
all: {
name: "All Times",
icon: <Sparkles />,
description: "Shows all posts",
},
};
return ( return (
<div> <div>
<div className="flex justify-between p-4 pb-0"> <div className="flex justify-between p-4 pb-0">
<div className="flex gap-2"> <div className="flex gap-2">
<Dropdown backdrop="opaque"> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
size="sm" size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white" className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
variant="faded" variant="faded"
> >
{sorts[sort]?.name} {sort.charAt(0).toUpperCase() + sort.slice(1)}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
@ -193,47 +87,14 @@ export default function Posts() {
}} }}
className="text-[#333] dark:text-white" className="text-[#333] dark:text-white"
> >
{Object.entries(sorts).map(([key, sort]) => ( <DropdownItem key="newest">Newest</DropdownItem>
<DropdownItem <DropdownItem key="top">Top</DropdownItem>
key={key} <DropdownItem key="oldest">Oldest</DropdownItem>
startContent={sort.icon}
description={sort.description}
>
{sort.name}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{times[time]?.name}
</Button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
setTime(key as PostTime);
}}
className="text-[#333] dark:text-white"
>
{Object.entries(times).map(([key, sort]) => (
<DropdownItem
key={key}
startContent={sort.icon}
description={sort.description}
>
{sort.name}
</DropdownItem>
))}
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
<Button <Button
size="sm" size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white" className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
variant="faded" variant="faded"
onPress={() => { onPress={() => {
toast.warning("Flair filtering functionality coming soon"); toast.warning("Flair filtering functionality coming soon");
@ -243,11 +104,11 @@ export default function Posts() {
</Button> </Button>
</div> </div>
<div> <div>
<Dropdown backdrop="opaque"> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
size="sm" size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white" className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
variant="faded" variant="faded"
> >
{style.charAt(0).toUpperCase() + style.slice(1)} {style.charAt(0).toUpperCase() + style.slice(1)}
@ -277,15 +138,10 @@ export default function Posts() {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-3 p-4"> <div className="flex flex-col gap-3 p-4">
{posts && posts.length > 0 ? ( {posts &&
posts.map((post) => ( posts.map((post) => (
<PostCard key={post.id} post={post} style={style} user={user} /> <PostCard key={post.id} post={post} style={style} user={user} />
)) ))}
) : (
<p className="text-center text-[#333] dark:text-white transition-color duration-250 ease-linear">
No posts match your filters
</p>
)}
</div> </div>
)} )}
</div> </div>

View file

@ -2,8 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FeaturedStreamerType } from "@/types/FeaturedStreamerType"; import { FeaturedStreamerType } from "@/types/FeaturedStreamerType";
import { Image } from "@nextui-org/react";
import NextImage from "next/image";
export default function Streams() { export default function Streams() {
const [streamers, setStreamers] = useState<FeaturedStreamerType[]>([]); const [streamers, setStreamers] = useState<FeaturedStreamerType[]>([]);
@ -52,7 +51,7 @@ export default function Streams() {
const currentStreamer = streamers[currentIndex]; // Get the currently displayed streamer const currentStreamer = streamers[currentIndex]; // Get the currently displayed streamer
return ( return (
<div className="text-[#333] dark:text-white text-center p-6 transition-color duration-250"> <div style={{ textAlign: "center", padding: "20px" }}>
<h1>Featured Streamer</h1> <h1>Featured Streamer</h1>
<div <div
style={{ style={{
@ -66,30 +65,15 @@ export default function Streams() {
margin: "0 auto", margin: "0 auto",
}} }}
> >
<Image <img
as={NextImage}
src={currentStreamer.thumbnailUrl} src={currentStreamer.thumbnailUrl}
alt={`${currentStreamer.userName}'s thumbnail`} alt={`${currentStreamer.userName}'s thumbnail`}
style={{ width: "100%", borderRadius: "4px", marginBottom: "10px" }} style={{ width: "100%", borderRadius: "4px", marginBottom: "10px" }}
width={320}
height={180}
/> />
<a <a href={`https://twitch.tv/${currentStreamer.userName}`} target="_blank" ><div style={{height:"100px",display:"flex", flexDirection:"column",justifyContent:"center"}}>
href={`https://twitch.tv/${currentStreamer.userName}`} <h3>{currentStreamer.userName}</h3>
target="_blank" <p>{currentStreamer.streamTitle}</p>
> </div></a>
<div
style={{
height: "100px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
<h3>{currentStreamer.userName}</h3>
<p>{currentStreamer.streamTitle}</p>
</div>
</a>
<div> <div>
{currentStreamer.streamTags.map((tag, index) => ( {currentStreamer.streamTags.map((tag, index) => (
<span <span

View file

@ -8,31 +8,16 @@ export default function ThemeToggle() {
const [isSpinning, setIsSpinning] = useState<boolean>(false); const [isSpinning, setIsSpinning] = useState<boolean>(false);
const { setTheme, resolvedTheme } = useTheme(); const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState<boolean>(false); const [mounted, setMounted] = useState<boolean>(false);
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []); }, []);
const handleToggle = () => { const handleToggle = () => {
if (isSpinning) return; if (isSpinning) return;
if (!reduceMotion) { setIsSpinning(true);
setIsSpinning(true); setTimeout(() => setIsSpinning(false), 500);
setTimeout(() => setIsSpinning(false), 500);
}
setTheme(resolvedTheme === "dark" ? "light" : "dark"); setTheme(resolvedTheme === "dark" ? "light" : "dark");
}; };
@ -45,15 +30,9 @@ export default function ThemeToggle() {
<div <div
onClick={handleToggle} onClick={handleToggle}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
className={`${ className={`${isSpinning && "animate-[spin_0.5s_ease-out]"} `}
isSpinning && !reduceMotion ? "animate-[spin_0.5s_ease-out]" : ""
}`}
> >
<div <div className="duration-500 ease-in-out transition-all transform text-[#333] dark:text-white hover:scale-125">
className={`!duration-250 !ease-linear !transition-all transform text-[#333] dark:text-white ${
!reduceMotion ? "hover:scale-125" : ""
}`}
>
{resolvedTheme === "dark" && <Moon />} {resolvedTheme === "dark" && <Moon />}
{resolvedTheme === "light" && <Sun />} {resolvedTheme === "light" && <Sun />}
</div> </div>

View file

@ -10,11 +10,6 @@ export default function Timer({
targetDate: Date; targetDate: Date;
}) { }) {
const [timeLeft, setTimeLeft] = useState(targetDate.getTime() - Date.now()); const [timeLeft, setTimeLeft] = useState(targetDate.getTime() - Date.now());
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@ -34,24 +29,16 @@ export default function Timer({
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000)); const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
const days = Math.floor(totalSeconds / 86400); const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600); const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60); // const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60; // const seconds = totalSeconds % 60;
return [ return `${days} days ${hours.toString()} hours`;
`${days} days ${hours.toString()} hours`,
`${minutes.toString()} minutes ${seconds.toString()} seconds`,
];
}; };
if (!mounted) {
return null;
}
return ( return (
<div> <div>
<p>{name}</p> <p>{name}</p>
<p className="text-4xl text-wrap">{formatTime(timeLeft)[0]}</p> <p className="text-4xl">{formatTime(timeLeft)}</p>
<p className="text-4xl text-wrap">{formatTime(timeLeft)[1]}</p>
</div> </div>
); );
} }

View file

@ -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 transition-color duration-250"> <div className="text-[#333] dark:text-white ease-in-out transition-color duration-500">
<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")}

View file

@ -1 +1 @@
export type PostSort = "newest" | "oldest" | "top"; export type PostSort = "newest" | "oldest";

View file

@ -1,13 +0,0 @@
export type PostTime =
| "hour"
| "three_hours"
| "six_hours"
| "twelve_hours"
| "day"
| "week"
| "month"
| "three_months"
| "six_months"
| "nine_months"
| "year"
| "all";