Compare commits

...

3 commits

Author SHA1 Message Date
Ategon
a5c8ba0e34 Add image previews 2025-02-07 22:52:13 -05:00
Ategon
6a1f3abaa5 Add more user settings 2025-02-07 22:41:10 -05:00
Ategon
7ff860f3cc Fix bug where tags couldnt sort when logged out 2025-02-07 15:38:15 -05:00
6 changed files with 128 additions and 29 deletions

View file

@ -5,10 +5,7 @@ const nextConfig: NextConfig = {
remotePatterns: [
{
protocol: "https",
hostname: "static-cdn.jtvnw.net",
port: "",
pathname: "/**",
search: "",
hostname: "**",
},
],
},

View file

@ -1,17 +1,25 @@
"use client";
import Editor from "@/components/editor";
import sanitizeHtml from "sanitize-html";
import { getCookie, hasCookie } from "@/helpers/cookie";
import { UserType } from "@/types/UserType";
import { Button, Form, Input } from "@nextui-org/react";
import { Avatar, Button, Form, Input } from "@nextui-org/react";
import { redirect, usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { LoaderCircle } from "lucide-react";
import Image from "next/image";
export default function UserPage() {
const [user, setUser] = useState<UserType>();
const [profilePicture, setProfilePicture] = useState("");
const [name, setName] = useState("");
const [bannerPicture, setBannerPicture] = useState("");
const [bio, setBio] = useState("");
const [errors] = useState({});
const pathname = usePathname();
const [waitingSave, setWaitingSave] = useState(false);
useEffect(() => {
loadUser();
@ -37,6 +45,9 @@ export default function UserPage() {
setUser(data);
setProfilePicture(data.profilePicture ?? "");
setBannerPicture(data.bannerPicture ?? "");
setBio(data.bio ?? "");
setName(data.name ?? "");
} else {
setUser(undefined);
}
@ -44,19 +55,31 @@ export default function UserPage() {
}, [pathname]);
return (
<div className="absolute flex items-center justify-center top-0 left-0 w-screen h-screen">
<div className="flex items-center justify-center">
{!user ? (
"Loading settings..."
) : (
<Form
className="w-full max-w-xs flex flex-col gap-4"
className="w-full max-w-2xl flex flex-col gap-4"
validationErrors={errors}
onReset={() => {
setProfilePicture(user.profilePicture ?? "");
setBannerPicture(user.bannerPicture ?? "");
setBio(user.bio ?? "");
setName(user.name ?? "");
}}
onSubmit={async (e) => {
e.preventDefault();
const sanitizedBio = sanitizeHtml(bio);
if (!name) {
toast.error("You need to enter a name");
return;
}
setWaitingSave(true);
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/user"
@ -64,7 +87,10 @@ export default function UserPage() {
{
body: JSON.stringify({
slug: user.slug,
name: name,
bio: sanitizedBio,
profilePicture: profilePicture,
bannerPicture: bannerPicture,
}),
method: "PUT",
headers: {
@ -77,11 +103,28 @@ export default function UserPage() {
if (response.ok) {
toast.success("Changed settings");
setUser(await response.json());
setWaitingSave(false);
} else {
toast.error("Failed to update settings");
setWaitingSave(false);
}
}}
>
<p className="text-3xl">Settings</p>
<Input
label="Name"
labelPlacement="outside"
name="name"
placeholder="Enter a name"
type="text"
value={name}
onValueChange={setName}
/>
<p>Bio</p>
<Editor content={bio} setContent={setBio} />
<Input
label="Profile Picture"
labelPlacement="outside"
@ -92,9 +135,38 @@ export default function UserPage() {
onValueChange={setProfilePicture}
/>
{profilePicture && <Avatar src={profilePicture} />}
<Input
label="Banner Picture"
labelPlacement="outside"
name="bannerPicture"
placeholder="Enter a url to an image"
type="text"
value={bannerPicture}
onValueChange={setBannerPicture}
/>
{bannerPicture &&
bannerPicture.startsWith("https://") &&
bannerPicture.length > 8 && (
<div className="bg-[#222222] h-28 w-full relative">
<Image
src={bannerPicture}
alt={`${user.name}'s profile banner`}
className="object-cover"
fill
/>
</div>
)}
<div className="flex gap-2">
<Button color="primary" type="submit">
Save
{waitingSave ? (
<LoaderCircle className="animate-spin" size={16} />
) : (
<p>Save</p>
)}
</Button>
<Button type="reset" variant="flat">
Reset

View file

@ -1,6 +1,8 @@
"use client";
import { UserType } from "@/types/UserType";
import { Avatar } from "@nextui-org/react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
@ -24,8 +26,31 @@ export default function UserPage() {
return (
<div>
{user && (
<div>
<p>{user.name}</p>
<div className="border-2 border-[#222224] relative rounded-xl overflow-hidden bg-[#18181a]">
<div className="bg-[#222222] h-28 relative">
{user.bannerPicture && (
<Image
src={user.bannerPicture}
alt={`${user.name}'s profile banner`}
className="object-cover"
fill
/>
)}
</div>
<Avatar
className="absolute rounded-full left-16 top-16 h-24 w-24 bg-transparent"
src={user.profilePicture}
/>
<div className="p-8 mt-8">
<p className="text-3xl">{user.name}</p>
<div
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
dangerouslySetInnerHTML={{
__html:
user.bio && user.bio != "<p></p>" ? user.bio : "No user bio",
}}
/>
</div>
</div>
)}
</div>

View file

@ -3,6 +3,7 @@ import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownSection,
DropdownTrigger,
NavbarItem,
} from "@nextui-org/react";
@ -26,21 +27,23 @@ export default function PCNavbarUser({ user }: NavbarUserProps) {
/>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem
key="profile"
className="text-[#333] dark:text-white"
href={`/u/${user.slug}`}
>
Profile
</DropdownItem>
<DropdownItem
showDivider
key="settings"
className="text-[#333] dark:text-white"
href="/settings"
>
Settings
</DropdownItem>
<DropdownSection title={user.name}>
<DropdownItem
key="profile"
className="text-[#333] dark:text-white"
href={`/u/${user.slug}`}
>
Profile
</DropdownItem>
<DropdownItem
showDivider
key="settings"
className="text-[#333] dark:text-white"
href="/settings"
>
Settings
</DropdownItem>
</DropdownSection>
<DropdownItem
key="logout"
color="danger"

View file

@ -181,14 +181,14 @@ export default function Posts() {
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.map((key) => `${key}`)
.join("_")
: ""
}`
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.map((key) => `${key}`)
.join("_")
: ""
}`
@ -201,14 +201,14 @@ export default function Posts() {
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.map((key) => `${key}`)
.join("_")
: ""
}&sticky=true`
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.map((key) => `${key}`)
.join("_")
: ""
}&sticky=true`

View file

@ -2,7 +2,9 @@ export interface UserType {
id: number;
slug: string;
name: string;
bio: string;
profilePicture: string;
bannerPicture: string;
createdAt: Date;
mod: boolean;
admin: boolean;