mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Compare commits
12 commits
f26af125b8
...
aefb9e2018
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aefb9e2018 | ||
![]() |
b11ad9354b | ||
![]() |
feb80916cc | ||
![]() |
3df39a4405 | ||
![]() |
7de5732596 | ||
![]() |
b816523cf9 | ||
![]() |
743c217ab2 | ||
![]() |
2325d648d9 | ||
![]() |
ce224af649 | ||
![]() |
373aff3911 | ||
![]() |
d9daea6ece | ||
![]() |
68b45a57b8 |
25 changed files with 469 additions and 114 deletions
next.config.ts
public/images
src
app
components
editor
jam-header
link-components
navbar
posts
streams
theme-toggle
timers
types
|
@ -1,7 +1,17 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "static-cdn.jtvnw.net",
|
||||||
|
port: "",
|
||||||
|
pathname: "/**",
|
||||||
|
search: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
BIN
public/images/D2J_Icon.png
Normal file
BIN
public/images/D2J_Icon.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 24 KiB |
Binary file not shown.
Before ![]() (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 |
|
@ -60,7 +60,6 @@ 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");
|
||||||
|
@ -105,7 +104,7 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
<p className="text-[#333] dark:text-white transition-color duration-250">
|
||||||
Don't have an account? <Link href="/signup">Sign up</Link>
|
Don't have an account? <Link href="/signup">Sign up</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -6,11 +6,28 @@ import { toast } from "react-toastify";
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
"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("");
|
||||||
|
@ -72,6 +74,7 @@ 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",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -81,13 +84,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
|
||||||
|
@ -129,7 +132,7 @@ export default function UserPage() {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#333] dark:text-white transition-color duration-500 ease-in-out">
|
<p className="text-[#333] dark:text-white transition-color duration-250">
|
||||||
Already have an account? <Link href="/login">Log In</Link>
|
Already have an account? <Link href="/login">Log In</Link>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
@ -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 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
|
: 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-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">
|
<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="transform-all duration-500 ease-in-out"
|
className="!duration-250 !ease-linear !transition-all"
|
||||||
/>
|
/>
|
||||||
<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="transform-all duration-500 ease-in-out"
|
className="!duration-250 !ease-linear !transition-all"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{editor.storage.characterCount.characters()} / {limit} characters
|
{editor.storage.characterCount.characters()} / {limit} characters
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
export default function JamHeader() {
|
export default function JamHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#7090b9] dark:bg-[#124a88] flex rounded-2xl overflow-hidden text-white transition-color duration-500 ease-in-out">
|
<div className="bg-[#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-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">
|
||||||
<Calendar />
|
<Calendar />
|
||||||
<p>Dare2Jam 1</p>
|
<p>Dare2Jam 1</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@nextui-org/react";
|
import { Button } from "@nextui-org/react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface ButtonActionProps {
|
interface ButtonActionProps {
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
@ -12,10 +14,28 @@ 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 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"
|
variant="bordered"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Button, Link } from "@nextui-org/react";
|
import { Button, Link } from "@nextui-org/react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface ButtonLinkProps {
|
interface ButtonLinkProps {
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
@ -8,14 +10,34 @@ 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 hover:scale-110"
|
className={`flex justify-center duration-500 ease-in-out transition-all transform ${
|
||||||
|
!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}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Link } from "@nextui-org/react";
|
import { Link } from "@nextui-org/react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface IconLinkProps {
|
interface IconLinkProps {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
|
@ -7,10 +9,28 @@ 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 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}
|
{icon}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
"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;
|
||||||
|
@ -6,10 +9,28 @@ 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 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}
|
{name}
|
||||||
</BaseLink>
|
</BaseLink>
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default function MobileNavbar() {
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
as={NextImage}
|
as={NextImage}
|
||||||
src="/images/hereyougopomo.png"
|
src="/images/D2J_Icon.png"
|
||||||
className="min-w-[70px]"
|
className="min-w-[70px]"
|
||||||
alt="Dare2Jam logo"
|
alt="Dare2Jam logo"
|
||||||
width={70}
|
width={70}
|
||||||
|
|
|
@ -38,6 +38,21 @@ 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();
|
||||||
|
@ -91,11 +106,13 @@ 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 hover:scale-110"
|
className={`duration-500 ease-in-out transition-all transform ${
|
||||||
|
reduceMotion ? "" : "hover:scale-110"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
as={NextImage}
|
as={NextImage}
|
||||||
src="/images/hereyougopomo.png"
|
src="/images/D2J_Icon.png"
|
||||||
className="min-w-[70px]"
|
className="min-w-[70px]"
|
||||||
alt="Dare2Jam logo"
|
alt="Dare2Jam logo"
|
||||||
width={70}
|
width={70}
|
||||||
|
|
|
@ -15,7 +15,7 @@ interface NavbarUserProps {
|
||||||
export default function PCNavbarUser({ user }: NavbarUserProps) {
|
export default function PCNavbarUser({ user }: NavbarUserProps) {
|
||||||
return (
|
return (
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<Dropdown>
|
<Dropdown backdrop="opaque">
|
||||||
<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-black"
|
className="text-[#333] dark:text-white"
|
||||||
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-black"
|
className="text-[#333] dark:text-white"
|
||||||
href="/settings"
|
href="/settings"
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
|
|
|
@ -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 } from "react";
|
import { useState, useEffect } 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,6 +14,21 @@ 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
|
||||||
|
@ -56,7 +71,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 occured");
|
toast.error("An error occurred");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,7 +94,11 @@ export default function LikeButton({ post }: { post: PostType }) {
|
||||||
<Heart size={16} />
|
<Heart size={16} />
|
||||||
<Heart
|
<Heart
|
||||||
size={16}
|
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={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "0",
|
top: "0",
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default function PostCard({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<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" }}
|
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 transition-all !duration-500 ease-in-out"
|
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
|
||||||
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>
|
<Dropdown backdrop="opaque">
|
||||||
<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>
|
||||||
<DropdownSection title="Mod Zone">
|
{user?.mod ? (
|
||||||
{user?.mod ? (
|
<DropdownSection title="Mod Zone">
|
||||||
<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"
|
||||||
{user?.admin && !post.author.mod ? (
|
startContent={<Shield />}
|
||||||
<DropdownItem
|
description="Promote user to Mod"
|
||||||
key="promote-mod"
|
>
|
||||||
startContent={<Shield />}
|
Appoint as mod
|
||||||
description="Promote user to Mod"
|
</DropdownItem>
|
||||||
>
|
) : (
|
||||||
Appoint as mod
|
<></>
|
||||||
</DropdownItem>
|
)}
|
||||||
) : (
|
{user?.admin &&
|
||||||
<></>
|
post.author.mod &&
|
||||||
)}
|
post.author.id !== user.id ? (
|
||||||
{user?.admin &&
|
<DropdownItem
|
||||||
post.author.mod &&
|
key="demote-mod"
|
||||||
post.author.id !== user.id ? (
|
startContent={<ShieldX />}
|
||||||
<DropdownItem
|
description="Demote user from Mod"
|
||||||
key="demote-mod"
|
>
|
||||||
startContent={<ShieldX />}
|
Remove as mod
|
||||||
description="Demote user from Mod"
|
</DropdownItem>
|
||||||
>
|
) : (
|
||||||
Remove as mod
|
<></>
|
||||||
</DropdownItem>
|
)}
|
||||||
) : (
|
{user?.admin && !post.author.admin ? (
|
||||||
<></>
|
<DropdownItem
|
||||||
)}
|
key="promote-admin"
|
||||||
{user?.admin && !post.author.admin ? (
|
startContent={<ShieldAlert />}
|
||||||
<DropdownItem
|
description="Promote user to Admin"
|
||||||
key="promote-admin"
|
>
|
||||||
startContent={<ShieldAlert />}
|
Appoint as admin
|
||||||
description="Promote user to Admin"
|
</DropdownItem>
|
||||||
>
|
) : (
|
||||||
Appoint as admin
|
<></>
|
||||||
</DropdownItem>
|
)}
|
||||||
) : (
|
</DropdownSection>
|
||||||
<></>
|
) : (
|
||||||
)}
|
<></>
|
||||||
</DropdownSection>
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { ReactNode, 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,12 +14,31 @@ 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 { 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 { 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);
|
||||||
|
@ -45,8 +64,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}`
|
? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}`
|
||||||
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}`
|
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${userData.slug}&time=${time}`
|
||||||
);
|
);
|
||||||
setPosts(await postsResponse.json());
|
setPosts(await postsResponse.json());
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -56,8 +75,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}`
|
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}`
|
||||||
: `http://localhost:3005/api/v1/posts?sort=${sort}`
|
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}`
|
||||||
);
|
);
|
||||||
setPosts(await postsResponse.json());
|
setPosts(await postsResponse.json());
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -65,20 +84,107 @@ export default function Posts() {
|
||||||
};
|
};
|
||||||
|
|
||||||
loadUserAndPosts();
|
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 (
|
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>
|
<Dropdown backdrop="opaque">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
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"
|
variant="faded"
|
||||||
>
|
>
|
||||||
{sort.charAt(0).toUpperCase() + sort.slice(1)}
|
{sorts[sort]?.name}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
@ -87,14 +193,47 @@ export default function Posts() {
|
||||||
}}
|
}}
|
||||||
className="text-[#333] dark:text-white"
|
className="text-[#333] dark:text-white"
|
||||||
>
|
>
|
||||||
<DropdownItem key="newest">Newest</DropdownItem>
|
{Object.entries(sorts).map(([key, sort]) => (
|
||||||
<DropdownItem key="top">Top</DropdownItem>
|
<DropdownItem
|
||||||
<DropdownItem key="oldest">Oldest</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>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
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"
|
variant="faded"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
toast.warning("Flair filtering functionality coming soon");
|
toast.warning("Flair filtering functionality coming soon");
|
||||||
|
@ -104,11 +243,11 @@ export default function Posts() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Dropdown>
|
<Dropdown backdrop="opaque">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
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"
|
variant="faded"
|
||||||
>
|
>
|
||||||
{style.charAt(0).toUpperCase() + style.slice(1)}
|
{style.charAt(0).toUpperCase() + style.slice(1)}
|
||||||
|
@ -138,10 +277,15 @@ 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 && posts.length > 0 ? (
|
||||||
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>
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
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[]>([]);
|
||||||
|
@ -51,7 +52,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 style={{ textAlign: "center", padding: "20px" }}>
|
<div className="text-[#333] dark:text-white text-center p-6 transition-color duration-250">
|
||||||
<h1>Featured Streamer</h1>
|
<h1>Featured Streamer</h1>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -65,15 +66,30 @@ export default function Streams() {
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
|
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 href={`https://twitch.tv/${currentStreamer.userName}`} target="_blank" ><div style={{height:"100px",display:"flex", flexDirection:"column",justifyContent:"center"}}>
|
<a
|
||||||
<h3>{currentStreamer.userName}</h3>
|
href={`https://twitch.tv/${currentStreamer.userName}`}
|
||||||
<p>{currentStreamer.streamTitle}</p>
|
target="_blank"
|
||||||
</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
|
||||||
|
|
|
@ -8,16 +8,31 @@ 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;
|
||||||
|
|
||||||
setIsSpinning(true);
|
if (!reduceMotion) {
|
||||||
setTimeout(() => setIsSpinning(false), 500);
|
setIsSpinning(true);
|
||||||
|
setTimeout(() => setIsSpinning(false), 500);
|
||||||
|
}
|
||||||
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||||
};
|
};
|
||||||
|
@ -30,9 +45,15 @@ export default function ThemeToggle() {
|
||||||
<div
|
<div
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
style={{ cursor: "pointer" }}
|
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 === "dark" && <Moon />}
|
||||||
{resolvedTheme === "light" && <Sun />}
|
{resolvedTheme === "light" && <Sun />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,11 @@ 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(() => {
|
||||||
|
@ -29,16 +34,24 @@ 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 `${days} days ${hours.toString()} hours`;
|
return [
|
||||||
|
`${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">{formatTime(timeLeft)}</p>
|
<p className="text-4xl text-wrap">{formatTime(timeLeft)[0]}</p>
|
||||||
|
<p className="text-4xl text-wrap">{formatTime(timeLeft)[1]}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Timer from "./Timer";
|
||||||
|
|
||||||
export default function Timers() {
|
export default function Timers() {
|
||||||
return (
|
return (
|
||||||
<div className="text-[#333] dark:text-white ease-in-out transition-color duration-500">
|
<div className="text-[#333] dark:text-white transition-color duration-250">
|
||||||
<Timer
|
<Timer
|
||||||
name="Jam Start"
|
name="Jam Start"
|
||||||
targetDate={new Date("2025-04-04T18:00:00-05:00")}
|
targetDate={new Date("2025-04-04T18:00:00-05:00")}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export type PostSort = "newest" | "oldest";
|
export type PostSort = "newest" | "oldest" | "top";
|
||||||
|
|
13
src/types/PostTimes.ts
Normal file
13
src/types/PostTimes.ts
Normal 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";
|
Loading…
Reference in a new issue