mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Enable avatar upload for users
This commit is contained in:
@@ -4,7 +4,7 @@ import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@ne
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const {state, logout} = useAuth();
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const profileMenuItems = [
|
||||
@@ -17,7 +17,7 @@ export default function ProfileMenu() {
|
||||
label: "Administration",
|
||||
icon: <GearFine/>,
|
||||
onClick: () => navigate("/administration/libraries"),
|
||||
showIf: state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||
showIf: auth.state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
@@ -27,7 +27,7 @@ export default function ProfileMenu() {
|
||||
{
|
||||
label: "Sign Out",
|
||||
icon: <SignOut/>,
|
||||
onClick: () => logout(),
|
||||
onClick: () => auth.logout(),
|
||||
color: "primary"
|
||||
},
|
||||
];
|
||||
@@ -36,6 +36,7 @@ export default function ProfileMenu() {
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar showFallback
|
||||
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||
radius="full"
|
||||
as="button"
|
||||
className="transition-transform size-8"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import Input from "Frontend/components/general/Input";
|
||||
import {Form, Formik} from "formik";
|
||||
import {Button} from "@nextui-org/react";
|
||||
import {Avatar, Button} from "@nextui-org/react";
|
||||
import {Check, Info} from "@phosphor-icons/react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
@@ -10,6 +10,7 @@ import UserUpdateDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserU
|
||||
import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
import {toast} from "sonner";
|
||||
import FileUpload from "Frontend/components/general/FileUpload";
|
||||
|
||||
export default function ProfileManagement() {
|
||||
const [configSaved, setConfigSaved] = useState(false);
|
||||
@@ -90,13 +91,18 @@ export default function ProfileManagement() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-1 justify-between gap-8">
|
||||
<div className="flex flex-col basis-1/4 items-center">
|
||||
<Section title="Avatar"></Section>
|
||||
<Avatar showFallback
|
||||
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||
className="size-40 m-4"></Avatar>
|
||||
<FileUpload upload="/avatar/upload" clear="/avatar/delete" accept="image/*"/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-grow">
|
||||
<Section title="Personal information"/>
|
||||
<Input name="username" label="Username" type="text" autocomplete="username"/>
|
||||
<Input name="email" label="Email" type="email" autocomplete="email"/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1">
|
||||
<Section title="Security"/>
|
||||
<Input name="newPassword" label="New Password" type="password"
|
||||
autocomplete="new-password"/>
|
||||
|
||||
@@ -27,7 +27,7 @@ function UserManagementLayout({getConfig, formik}: any) {
|
||||
</div>
|
||||
|
||||
<Section title="Users"/>
|
||||
<div className="grid grid-flow-col grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-300px gap-4">
|
||||
{users.map((user) => <UserCard user={user} key={user.username}/>)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {toast} from "sonner";
|
||||
import {getCsrfToken} from "Frontend/util/auth";
|
||||
import {Button, Input, Tooltip} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
import {Trash} from "@phosphor-icons/react";
|
||||
|
||||
export default function FileUpload({upload, clear, accept}: { upload: string, clear: string, accept: string }) {
|
||||
|
||||
const [avatar, setAvatar] = useState<any>();
|
||||
|
||||
function onFileSelected(event: any) {
|
||||
setAvatar(event.target.files[0]);
|
||||
}
|
||||
|
||||
async function uploadAvatar() {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("file", avatar);
|
||||
try {
|
||||
const response = await fetch(upload, {
|
||||
headers: {
|
||||
"X-CSRF-Token": getCsrfToken()
|
||||
},
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.text();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Avatar updated");
|
||||
} else {
|
||||
toast.error("Error uploading avatar", {description: result});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("Error uploading avatar", {description: error.message})
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAvatar() {
|
||||
try {
|
||||
const response = await fetch(clear, {
|
||||
headers: {
|
||||
"X-CSRF-Token": getCsrfToken()
|
||||
},
|
||||
method: "POST",
|
||||
credentials: "same-origin"
|
||||
});
|
||||
|
||||
const result = await response.text();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Avatar removed");
|
||||
} else {
|
||||
toast.error("Error removing avatar", {description: result});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("Error removing avatar", {description: error.message})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<Input type="file" accept={accept} onChange={onFileSelected}/>
|
||||
<Button onClick={uploadAvatar} color="success">Upload</Button>
|
||||
<Tooltip content="Remove your current avatar">
|
||||
<Button onClick={removeAvatar} isIconOnly color="danger"><Trash/></Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import {Divider} from "@nextui-org/react";
|
||||
export default function Section({title}: { title: string }) {
|
||||
return (
|
||||
<>
|
||||
<h2 className={"text-xl font-bold mt-8"}>{title}</h2>
|
||||
<h2 className={"text-xl font-bold mt-8 mb-1"}>{title}</h2>
|
||||
<Divider className="mb-4"/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,11 +4,15 @@ import {roleToColor, roleToRoleName} from "Frontend/util/utils";
|
||||
|
||||
export function UserCard({user}: { user: UserInfoDto }) {
|
||||
return (
|
||||
<Card className="flex flex-row flex-grow items-center gap-4 p-2">
|
||||
<Avatar classNames={{
|
||||
base: "gradient-primary size-20",
|
||||
icon: "text-background/80"
|
||||
}}></Avatar>
|
||||
<Card className="flex flex-row items-center gap-4 p-2">
|
||||
<Avatar showFallback
|
||||
name={user.username?.charAt(0)}
|
||||
src={`/images/avatar?username=${user?.username}`}
|
||||
classNames={{
|
||||
base: "gradient-primary size-20",
|
||||
icon: "text-background/80",
|
||||
name: "text-background/80 text-5xl -mt-1",
|
||||
}}></Avatar>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-semibold">{user.username}</p>
|
||||
<p className="text-sm">{user.email}</p>
|
||||
|
||||
@@ -7,4 +7,10 @@ const auth = configureAuth(UserEndpoint.getUserInfo);
|
||||
// Export auth provider and useAuth hook, which are automatically
|
||||
// typed to the result of `UserInfoService.getUserInfo`
|
||||
export const useAuth = auth.useAuth;
|
||||
export const AuthProvider = auth.AuthProvider;
|
||||
export const AuthProvider = auth.AuthProvider;
|
||||
|
||||
|
||||
export function getCsrfToken() {
|
||||
const token = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
return token || '';
|
||||
}
|
||||
Reference in New Issue
Block a user