Skip to content

Commit

Permalink
feat: uploading/removing user avatars & banners
Browse files Browse the repository at this point in the history
  • Loading branch information
sunaurus committed Apr 14, 2024
1 parent 5628ced commit 61fa1b6
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 9 deletions.
13 changes: 5 additions & 8 deletions src/app/settings/ProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SettingsInputWithLabel } from "@/app/settings/SettingsInputWithLabel";
import { MyUserInfo } from "lemmy-js-client";
import { updateProfileAction } from "@/app/settings/loggedInUserActions";
import { SubmitButton } from "@/app/(ui)/button/SubmitButton";
import { ProfileImageInput } from "@/app/settings/ProfileImageInput";

export const ProfileForm = (props: { readonly loggedInUser: MyUserInfo }) => {
return (
Expand Down Expand Up @@ -33,19 +34,15 @@ export const ProfileForm = (props: { readonly loggedInUser: MyUserInfo }) => {
placeholder={"@user:example.com"}
type={"text"}
/>
<SettingsInputWithLabel
defaultValue={props.loggedInUser.local_user_view.person.avatar}
disabled={true}
<ProfileImageInput
currentUrl={props.loggedInUser.local_user_view.person.avatar}
inputId={"avatar"}
label={"Avatar"}
type={"text"}
/>
<SettingsInputWithLabel
defaultValue={props.loggedInUser.local_user_view.person.banner}
disabled={true}
<ProfileImageInput
currentUrl={props.loggedInUser.local_user_view.person.banner}
inputId={"banner"}
label={"Banner"}
type={"text"}
/>
<SubmitButton>{"Update profile"}</SubmitButton>
</form>
Expand Down
173 changes: 173 additions & 0 deletions src/app/settings/ProfileImageInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import classNames from "classnames";
import { Input } from "@/app/(ui)/form/Input";
import { Select } from "@/app/(ui)/form/Select";
import { MarkdownTextArea } from "@/app/(ui)/markdown/MarkdownTextArea";
import { Image } from "@/app/(ui)/Image";
import { ChangeEvent, MouseEvent, useState } from "react";
import {
deleteImageAction,
uploadImageAction,
} from "@/app/(ui)/markdown/imageActions";
import { Button } from "@/app/(ui)/button/Button";
import { TrashIcon } from "@heroicons/react/16/solid";
import {
removeUserAvatar,
removeUserBanner,
} from "@/app/settings/loggedInUserActions";
import { Spinner } from "@/app/(ui)/Spinner";

type Props = {
readonly inputId: string;
readonly label: "Avatar" | "Banner";
readonly className?: string;
readonly currentUrl?: string;
};

export const ProfileImageInput = (props: Props) => {
const [url, setUrl] = useState(props.currentUrl);
const [imageDeleteUrl, setImageDeleteUrl] = useState<string | null>(null);

const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);

const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.currentTarget.files?.[0];
if (file) {
setUploading(true);
setUploadError(null);
const form = new FormData();

form.set("image", file);

const res = await uploadImageAction(form);
if (res.url && res.delete_url) {
setUrl(res.url);
setImageDeleteUrl(res.delete_url);
} else {
// @ts-ignore
e.target.value = null;
setUrl(undefined);
setUploadError(`Error: ${res.msg}`);
}

setUploading(false);
}
};

const handleDeleteImage = async (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (imageDeleteUrl) {
await deleteImageAction(imageDeleteUrl);
}
if (props.label === "Avatar") {
await removeUserAvatar();
} else {
await removeUserBanner();
}

const imageInput = document.getElementById(
`${props.inputId}-file`,
)! as HTMLInputElement;
// @ts-ignore
imageInput.value = null;

setUrl("");
setImageDeleteUrl(null);
};

const handleUrlChange = (e: ChangeEvent<HTMLInputElement>) => {
setUrl(e.currentTarget.value);
};

return (
<div
className={classNames(
"flex max-w-96 flex-wrap items-center",
props.className,
)}
>
<div className={"flex items-center justify-between"}>
<label
className={classNames(
"block flex items-center gap-2 text-sm font-medium leading-6",
)}
htmlFor={props.inputId}
>
{props.label} {uploading && <Spinner />}{" "}
{uploadError && (
<span className={"text-sm font-normal text-rose-400"}>
{uploadError}
</span>
)}
</label>
</div>

<Input
disabled={uploading}
id={`${props.inputId}-file`}
name={`${props.inputId}-file`}
onChange={handleImageUpload}
type={"file"}
/>
<div className={"relative w-full"}>
{url && props.label === "Avatar" && (
<div className={"mt-2 flex items-end gap-4"}>
<div>
<div className={"relative h-[100px] w-[100px]"}>
<Image
alt={"Preview large"}
className={"rounded object-cover"}
fill={true}
sizes={"100px"}
src={url}
/>
</div>
<span className={"text-xs text-neutral-400"}>{"Large"}</span>
</div>
<div>
<div className={"relative h-[20px] w-[20px]"}>
<Image
alt={"Preview small"}
className={"rounded object-cover"}
fill={true}
sizes={"100px"}
src={url}
/>
</div>
<span className={"text-xs text-neutral-400"}>{"Small"}</span>
</div>
</div>
)}
{url && props.label === "Banner" && (
<div className={"mt-2 flex items-end gap-4"}>
<div className={"relative h-[100px] w-96"}>
<Image
alt={"Preview"}
className={"rounded object-cover"}
fill={true}
sizes={"400px"}
src={url}
/>
</div>
</div>
)}
{url && (
<Button
className={"absolute right-0 top-2 flex items-center gap-1"}
color={"danger"}
onClick={handleDeleteImage}
>
<TrashIcon className={"h-4"} />
{"Remove image"}
</Button>
)}
</div>
<input
className={"hidden"}
id={props.inputId}
name={props.inputId}
value={url}
/>
</div>
);
};
2 changes: 1 addition & 1 deletion src/app/settings/SettingsInputWithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const SettingsInputWithLabel = (
>
<div className={"flex items-center justify-between"}>
<label
className={classNames("block w-32 text-sm font-medium leading-6", {
className={classNames("block text-sm font-medium leading-6", {
"w-64": props.type === "checkbox",
})}
htmlFor={props.inputId}
Expand Down
18 changes: 18 additions & 0 deletions src/app/settings/loggedInUserActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const updateProfileAction = async (formData: FormData) => {
display_name: formData.get("display_name")?.toString(),
bio: formData.get("bio")?.toString(),
matrix_user_id: formData.get("matrix")?.toString(),
avatar: formData.get("avatar")?.toString(),
banner: formData.get("banner")?.toString(),
});
revalidatePath("/settings");
};
Expand Down Expand Up @@ -124,3 +126,19 @@ export const enable2faAction = async (formData: FormData) => {
revalidatePath("/");
redirect("/settings");
};

export const removeUserAvatar = async () => {
await apiClient.saveUserSettings({
avatar: "",
});
revalidatePath("/settings");
revalidatePath("/");
};

export const removeUserBanner = async () => {
await apiClient.saveUserSettings({
banner: "",
});
revalidatePath("/settings");
revalidatePath("/");
};

0 comments on commit 61fa1b6

Please sign in to comment.