Compare commits

...

12 commits

Author SHA1 Message Date
Ategon
aefb9e2018 Add dropdown backdrop 2025-01-20 01:02:02 -05:00
Ategon
b11ad9354b Add prefers reduced motion handling 2025-01-20 00:57:59 -05:00
Ategon
feb80916cc Add post times 2025-01-20 00:37:30 -05:00
Ategon
3df39a4405 Fix sign up 2025-01-19 20:25:14 -05:00
Ategon
7de5732596 Fix logout to actually logout 2025-01-19 20:18:47 -05:00
Ategon
b816523cf9 Only show mod zone if user is a mod 2025-01-19 19:57:50 -05:00
Ategon
743c217ab2 Update icon to more polished version 2025-01-19 19:30:01 -05:00
Ategon
2325d648d9 Fix timer hydration 2025-01-19 19:26:18 -05:00
Ategon
ce224af649 Fix color fading 2025-01-19 19:03:37 -05:00
Ategon
373aff3911 adjust streamers font color 2025-01-19 18:28:11 -05:00
Ategon
d9daea6ece Merge branch 'main' of https://github.com/Ategon/Jamjar 2025-01-19 18:06:35 -05:00
Ategon
68b45a57b8 fix icon on light theme 2025-01-19 18:06:32 -05:00
25 changed files with 469 additions and 114 deletions

View file

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

BIN
public/images/D2J_Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

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

View file

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

View file

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

View file

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

View file

@ -2,8 +2,8 @@ import { Calendar } from "lucide-react";
export default function JamHeader() {
return (
<div className="bg-[#7090b9] dark:bg-[#124a88] flex rounded-2xl overflow-hidden text-white transition-color duration-500 ease-in-out">
<div className="bg-[#85bdd2] dark:bg-[#1892b3] p-4 px-6 flex items-center gap-2 font-bold transition-color duration-500 ease-in-out">
<div className="bg-[#7090b9] dark:bg-[#124a88] flex rounded-2xl overflow-hidden text-white 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-250">
<Calendar />
<p>Dare2Jam 1</p>
</div>

View file

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

View file

@ -1,5 +1,7 @@
"use client";
import { Button, Link } from "@nextui-org/react";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
interface ButtonLinkProps {
icon?: ReactNode;
@ -8,14 +10,34 @@ interface 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 (
<Link
href={href}
className="flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110"
className={`flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-110" : ""
}`}
>
<Button
endContent={icon}
className="text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out"
className={`text-[#333] dark:text-white border-[#333]/50 dark:border-white/50 transition-all transform !duration-500 ease-in-out ${
!reduceMotion ? "hover:scale-110" : ""
}`}
variant="bordered"
>
{name}

View file

@ -1,5 +1,7 @@
"use client";
import { Link } from "@nextui-org/react";
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
interface IconLinkProps {
icon: ReactNode;
@ -7,10 +9,28 @@ interface 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 (
<Link
href={href}
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-125 duration-500 ease-in-out transition-color"
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-125" : ""
} transition-color`}
>
{icon}
</Link>

View file

@ -1,4 +1,7 @@
"use client";
import { Link as BaseLink } from "@nextui-org/react";
import { useEffect, useState } from "react";
interface LinkProps {
name: string;
@ -6,10 +9,28 @@ interface 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 (
<BaseLink
href={href}
className="text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform hover:scale-110 duration-500 ease-in-out transition-color"
className={`text-[#333] dark:text-white flex justify-center duration-500 ease-in-out transition-all transform ${
!reduceMotion ? "hover:scale-110" : ""
} transition-color`}
>
{name}
</BaseLink>

View file

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

View file

@ -38,6 +38,21 @@ export default function PCNavbar() {
const [jam, setJam] = useState<JamType | null>();
const [isInJam, setIsInJam] = useState<boolean>();
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(() => {
loadUser();
@ -91,11 +106,13 @@ export default function PCNavbar() {
<NavbarBrand className="flex-grow-0">
<Link
href="/"
className="duration-500 ease-in-out transition-all transform hover:scale-110"
className={`duration-500 ease-in-out transition-all transform ${
reduceMotion ? "" : "hover:scale-110"
}`}
>
<Image
as={NextImage}
src="/images/hereyougopomo.png"
src="/images/D2J_Icon.png"
className="min-w-[70px]"
alt="Dare2Jam logo"
width={70}

View file

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

View file

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

View file

@ -46,7 +46,7 @@ export default function PostCard({
return (
<Card
className="bg-opacity-60 !duration-500 ease-in-out !transition-all"
className="bg-opacity-60 !duration-250 !ease-linear !transition-all"
style={{ display: hidden ? "none" : "flex" }}
>
<CardBody className="p-5">
@ -128,7 +128,7 @@ export default function PostCard({
<Spacer y={4} />
<div
className="prose dark:prose-invert transition-all !duration-500 ease-in-out"
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
@ -145,7 +145,7 @@ export default function PostCard({
>
<MessageCircle size={16} /> {0}
</Button>
<Dropdown>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button size="sm" variant="bordered" isIconOnly>
<MoreVertical size={16} />
@ -201,8 +201,8 @@ export default function PostCard({
<></>
)}
</DropdownSection>
<DropdownSection title="Mod Zone">
{user?.mod ? (
{user?.mod ? (
<DropdownSection title="Mod Zone">
<DropdownItem
key="remove"
startContent={<X />}
@ -236,45 +236,45 @@ export default function PostCard({
>
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>
{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>

View file

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import PostCard from "./PostCard";
import { PostType } from "@/types/PostType";
import {
@ -14,12 +14,31 @@ 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 {
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 { PostTime } from "@/types/PostTimes";
export default function Posts() {
const [posts, setPosts] = useState<PostType[]>();
const [sort, setSort] = useState<PostSort>("newest");
const [time, setTime] = useState<PostTime>("all");
const [style, setStyle] = useState<PostStyle>("cozy");
const [user, setUser] = useState<UserType>();
const [loading, setLoading] = useState<boolean>(true);
@ -45,8 +64,8 @@ export default function Posts() {
// 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}`
? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}`
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}`
);
setPosts(await postsResponse.json());
setLoading(false);
@ -56,8 +75,8 @@ export default function Posts() {
// 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}`
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}`
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}`
);
setPosts(await postsResponse.json());
setLoading(false);
@ -65,20 +84,107 @@ export default function Posts() {
};
loadUserAndPosts();
}, [sort]);
}, [sort, time]);
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 (
<div>
<div className="flex justify-between p-4 pb-0">
<div className="flex gap-2">
<Dropdown>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{sort.charAt(0).toUpperCase() + sort.slice(1)}
{sorts[sort]?.name}
</Button>
</DropdownTrigger>
<DropdownMenu
@ -87,14 +193,47 @@ export default function Posts() {
}}
className="text-[#333] dark:text-white"
>
<DropdownItem key="newest">Newest</DropdownItem>
<DropdownItem key="top">Top</DropdownItem>
<DropdownItem key="oldest">Oldest</DropdownItem>
{Object.entries(sorts).map(([key, sort]) => (
<DropdownItem
key={key}
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>
</Dropdown>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
onPress={() => {
toast.warning("Flair filtering functionality coming soon");
@ -104,11 +243,11 @@ export default function Posts() {
</Button>
</div>
<div>
<Dropdown>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] transition-all !duration-500 ease-in-out text-[#333] dark:text-white"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{style.charAt(0).toUpperCase() + style.slice(1)}
@ -138,10 +277,15 @@ export default function Posts() {
</div>
) : (
<div className="flex flex-col gap-3 p-4">
{posts &&
{posts && posts.length > 0 ? (
posts.map((post) => (
<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>

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import Timer from "./Timer";
export default function Timers() {
return (
<div className="text-[#333] dark:text-white ease-in-out transition-color duration-500">
<div className="text-[#333] dark:text-white transition-color duration-250">
<Timer
name="Jam Start"
targetDate={new Date("2025-04-04T18:00:00-05:00")}

View file

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

13
src/types/PostTimes.ts Normal file
View file

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