Add more user settings

This commit is contained in:
Ategon 2025-02-07 22:41:10 -05:00
parent 7ff860f3cc
commit 6a1f3abaa5
5 changed files with 106 additions and 23 deletions

View file

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

View file

@ -1,17 +1,24 @@
"use client"; "use client";
import Editor from "@/components/editor";
import sanitizeHtml from "sanitize-html";
import { getCookie, hasCookie } from "@/helpers/cookie"; import { getCookie, hasCookie } from "@/helpers/cookie";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { Button, Form, Input } from "@nextui-org/react"; import { Button, Form, Input } from "@nextui-org/react";
import { redirect, usePathname } from "next/navigation"; import { redirect, usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { LoaderCircle } from "lucide-react";
export default function UserPage() { export default function UserPage() {
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [profilePicture, setProfilePicture] = useState(""); const [profilePicture, setProfilePicture] = useState("");
const [name, setName] = useState("");
const [bannerPicture, setBannerPicture] = useState("");
const [bio, setBio] = useState("");
const [errors] = useState({}); const [errors] = useState({});
const pathname = usePathname(); const pathname = usePathname();
const [waitingSave, setWaitingSave] = useState(false);
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
@ -37,6 +44,9 @@ export default function UserPage() {
setUser(data); setUser(data);
setProfilePicture(data.profilePicture ?? ""); setProfilePicture(data.profilePicture ?? "");
setBannerPicture(data.bannerPicture ?? "");
setBio(data.bio ?? "");
setName(data.name ?? "");
} else { } else {
setUser(undefined); setUser(undefined);
} }
@ -49,14 +59,26 @@ export default function UserPage() {
"Loading settings..." "Loading settings..."
) : ( ) : (
<Form <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} validationErrors={errors}
onReset={() => { onReset={() => {
setProfilePicture(user.profilePicture ?? ""); setProfilePicture(user.profilePicture ?? "");
setBannerPicture(user.bannerPicture ?? "");
setBio(user.bio ?? "");
setName(user.name ?? "");
}} }}
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const sanitizedBio = sanitizeHtml(bio);
if (!name) {
toast.error("You need to enter a name");
return;
}
setWaitingSave(true);
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/user" ? "https://d2jam.com/api/v1/user"
@ -64,7 +86,10 @@ export default function UserPage() {
{ {
body: JSON.stringify({ body: JSON.stringify({
slug: user.slug, slug: user.slug,
name: name,
bio: sanitizedBio,
profilePicture: profilePicture, profilePicture: profilePicture,
bannerPicture: bannerPicture,
}), }),
method: "PUT", method: "PUT",
headers: { headers: {
@ -77,11 +102,28 @@ export default function UserPage() {
if (response.ok) { if (response.ok) {
toast.success("Changed settings"); toast.success("Changed settings");
setUser(await response.json()); setUser(await response.json());
setWaitingSave(false);
} else { } else {
toast.error("Failed to update settings"); 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 <Input
label="Profile Picture" label="Profile Picture"
labelPlacement="outside" labelPlacement="outside"
@ -92,9 +134,23 @@ export default function UserPage() {
onValueChange={setProfilePicture} onValueChange={setProfilePicture}
/> />
<Input
label="Banner Picture"
labelPlacement="outside"
name="bannerPicture"
placeholder="Enter a url to an image"
type="text"
value={bannerPicture}
onValueChange={setBannerPicture}
/>
<div className="flex gap-2"> <div className="flex gap-2">
<Button color="primary" type="submit"> <Button color="primary" type="submit">
Save {waitingSave ? (
<LoaderCircle className="animate-spin" size={16} />
) : (
<p>Save</p>
)}
</Button> </Button>
<Button type="reset" variant="flat"> <Button type="reset" variant="flat">
Reset Reset

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
import { Avatar } from "@nextui-org/react";
import Image from "next/image";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -24,8 +26,31 @@ export default function UserPage() {
return ( return (
<div> <div>
{user && ( {user && (
<div> <div className="border-2 border-[#222224] relative rounded-xl overflow-hidden bg-[#18181a]">
<p>{user.name}</p> <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>
)} )}
</div> </div>

View file

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

View file

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