From a2cdc63adcc9cd53486d00cbca6b2ecc0d82358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Znamen=C3=A1=C4=8Dek?= Date: Mon, 28 Oct 2024 10:57:41 +0100 Subject: [PATCH] Add first version of image upload component --- app/account/ImageUploader.tsx | 145 +++++++++++++++++++++++++++++++++ app/account/UserProfileTab.tsx | 34 +++++++- app/account/me/route.ts | 2 + next.config.js | 4 + 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 app/account/ImageUploader.tsx diff --git a/app/account/ImageUploader.tsx b/app/account/ImageUploader.tsx new file mode 100644 index 000000000..d788aa8d8 --- /dev/null +++ b/app/account/ImageUploader.tsx @@ -0,0 +1,145 @@ +import { useRef, useState, type FormEvent } from "react"; + +import { type PutBlobResult } from "@vercel/blob"; +import clsx from "clsx"; + +import { FormError } from "~/components/form/FormError"; +import { defaultAvatarUrl } from "~/src/utils"; + +type Props = { + setAvatarImage: (url: string) => void; + avatarImage: string; + onAvatarChange: (url: string) => void; +}; + +export const ImageUploader = ({ + setAvatarImage, + avatarImage, + onAvatarChange, +}: Props) => { + const inputFileRef = useRef(null); + const [uploading, setUploading] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [pendingChanges, setPendingChanges] = useState(false); + + const clearInputFile = () => { + if (inputFileRef.current) { + inputFileRef.current.value = ""; + } + }; + + const handleProblematicFile = (message: string) => { + setErrorMessage(message); + clearInputFile(); + setAvatarImage(defaultAvatarUrl); + setPendingChanges(false); + }; + + const onSubmit = async (event: FormEvent) => { + event.preventDefault(); + + if (!inputFileRef.current?.files) { + setErrorMessage("Není vybraný soubor"); + throw new Error("No file selected"); + } + + const file = inputFileRef.current.files[0]; + setUploading(true); + setPendingChanges(true); + const response = await fetch( + `/account/profile-picture?filename=${file.name}`, + { + method: "POST", + body: file, + }, + ); + + if (response.ok) { + const newBlob = (await response.json()) as PutBlobResult; + clearInputFile(); + onAvatarChange(newBlob.url); + } else { + setErrorMessage("Něco se nepovedlo, zkus to znovu"); + } + setUploading(false); + setPendingChanges(false); + }; + + const onUpload = () => { + setErrorMessage(""); + setPendingChanges(true); + if (!inputFileRef.current?.files) { + setErrorMessage("Není vybraný soubor"); + throw new Error("No file selected"); + } + + const file = inputFileRef.current.files[0]; + + if (file.size > 4500000) { + handleProblematicFile("Soubor musí být menší než 4.5 MB"); + return; + } + if (!file.type.startsWith("image/")) { + handleProblematicFile("Soubor musí mít formát obrázku"); + return; + } + const fileReader = new FileReader(); + fileReader.onload = (e) => { + if (e.target && typeof e.target.result === "string") { + setAvatarImage(e.target.result); + } else { + setErrorMessage("Něco se nepovedlo, zkus to znovu"); + throw new Error("FileReader result is not a string"); + } + }; + fileReader.readAsDataURL(file); + }; + + const onDelete = async () => { + clearInputFile(); + setAvatarImage(defaultAvatarUrl); + onAvatarChange(defaultAvatarUrl); + }; + + return ( +
+
+ + {errorMessage && ( +
+ +
+ )} +
+ + +
+
+
+ ); +}; diff --git a/app/account/UserProfileTab.tsx b/app/account/UserProfileTab.tsx index 4fce74966..4656cb4b3 100644 --- a/app/account/UserProfileTab.tsx +++ b/app/account/UserProfileTab.tsx @@ -5,6 +5,7 @@ import { type HTMLInputTypeAttribute, type InputHTMLAttributes, } from "react"; +import Image from "next/image"; import Link from "next/link"; import clsx from "clsx"; @@ -19,7 +20,9 @@ import { SkillSelect } from "~/components/profile/SkillSelect"; import { type UserProfile } from "~/src/data/user-profile"; import { setFlag } from "~/src/flags"; import { absolute, Route } from "~/src/routing"; -import { looksLikeEmailAdress } from "~/src/utils"; +import { defaultAvatarUrl, looksLikeEmailAdress } from "~/src/utils"; + +import { ImageUploader } from "./ImageUploader"; type SectionProps = { model?: UserProfile; @@ -50,6 +53,13 @@ export const UserProfileTab = () => { }; const BasicInfoSection = ({ model, updating, onChange }: SectionProps) => { + const [profilePictureUrl, setProfilePictureUrl] = useState< + string | undefined + >(); + useEffect(() => { + setProfilePictureUrl(model?.profilePictureUrl); + }, [model]); + return (

Základní informace

@@ -94,6 +104,28 @@ const BasicInfoSection = ({ model, updating, onChange }: SectionProps) => { onSave={(name) => onChange({ ...model!, name })} /> +
+ + Profilová foto +
+ { + onChange({ ...model!, profilePictureUrl: url }); + }} + /> +
+
+