mirror of
https://github.com/Ategon/Jamjar.git
synced 2025-02-12 06:16:21 +00:00
Compare commits
No commits in common. "aefb9e2018e7b8c1d0ee12bc5a389ac1e2315a4a" and "f26af125b8706bcb36d701926eb28e072457e2e0" have entirely different histories.
aefb9e2018
...
f26af125b8
25 changed files with 114 additions and 469 deletions
next.config.ts
public/images
src
app
components
editor
jam-header
link-components
navbar
posts
streams
theme-toggle
timers
types
|
@ -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 |
BIN
public/images/hereyougopomo.png
Normal file
BIN
public/images/hereyougopomo.png
Normal file
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 |
|
@ -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'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,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 (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export type PostSort = "newest" | "oldest" | "top";
|
export type PostSort = "newest" | "oldest";
|
||||||
|
|
|
@ -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";
|
|
Loading…
Reference in a new issue