Skip to content

Commit

Permalink
Add first version of image upload component
Browse files Browse the repository at this point in the history
  • Loading branch information
zoul committed Oct 28, 2024
1 parent 3b2e230 commit a2cdc63
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 1 deletion.
145 changes: 145 additions & 0 deletions app/account/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>(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<HTMLFormElement>) => {
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 (
<div>
<form onSubmit={onSubmit} className="flex flex-col gap-7">
<input
className="max-w-prose"
name="file"
ref={inputFileRef}
type="file"
required
onChange={onUpload}
/>
{errorMessage && (
<div className="py-1">
<FormError error={errorMessage} />
</div>
)}
<div className="flex gap-2">
<button
type="submit"
className={clsx(
uploading || !pendingChanges ? "btn-disabled" : "btn-primary",
)}
disabled={uploading}
>
Uložit fotku
</button>
<button
className={clsx(
uploading || avatarImage === defaultAvatarUrl
? "btn-disabled"
: "btn-inverted",
)}
onClick={onDelete}
disabled={uploading}
>
Smazat fotku
</button>
</div>
</form>
</div>
);
};
34 changes: 33 additions & 1 deletion app/account/UserProfileTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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 (
<section className="flex max-w-prose flex-col gap-7">
<h2 className="typo-title2">Základní informace</h2>
Expand Down Expand Up @@ -94,6 +104,28 @@ const BasicInfoSection = ({ model, updating, onChange }: SectionProps) => {
onSave={(name) => onChange({ ...model!, name })}
/>

<div className="flex flex-col gap-2">
<label htmlFor="avatarImage" className="block">
Profilová fotka:
</label>
<Image
src={profilePictureUrl ?? defaultAvatarUrl}
className="h-[100px] w-[100px] rounded-full bg-gray object-cover shadow"
alt="Profilová foto"
width={100}
height={100}
/>
<div className="space-y-2">
<ImageUploader
setAvatarImage={setProfilePictureUrl}
avatarImage={profilePictureUrl ?? defaultAvatarUrl}
onAvatarChange={(url: string) => {
onChange({ ...model!, profilePictureUrl: url });
}}
/>
</div>
</div>

<InputWithSaveButton
id="contactMail"
type="email"
Expand Down
2 changes: 2 additions & 0 deletions app/account/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function PATCH(request: NextRequest) {
maxSeniority,
occupation,
organizationName,
profilePictureUrl,
profileUrl,
} = await request.json();
await updateUserProfile(profile.id, {
Expand All @@ -100,6 +101,7 @@ export async function PATCH(request: NextRequest) {
privacyFlags,
contactEmail,
availableInDistricts,
profilePictureUrl,
bio,
tags,
maxSeniority,
Expand Down
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ module.exports = withAxiom({
protocol: "https",
hostname: "secure.gravatar.com",
},
{
protocol: "https",
hostname: "bbp30zne50ll9cz3.public.blob.vercel-storage.com",
},
],
},
async redirects() {
Expand Down

0 comments on commit a2cdc63

Please sign in to comment.