From 72361435ebf100eceb7f409cebe4dc449630422d Mon Sep 17 00:00:00 2001 From: rfhf19 <rfhf19@inf.ufpr.br> Date: Fri, 7 Feb 2025 09:39:45 -0300 Subject: [PATCH] Issue #227: ADD Image cropper to edit profile --- package.json | 1 + pnpm-lock.yaml | 18 +- src/app/components/ImageCropper.js | 185 +++++++++++++ src/app/components/NavigationBar.js | 42 +-- src/app/components/Overlay.js | 2 +- src/app/components/SideBar.js | 14 +- src/app/components/canvasPreview.js | 65 +++++ src/app/editar/[id]/components/EditForm.js | 9 +- src/app/editar/[id]/components/UpdateInfo.js | 243 +++++++++--------- src/app/editar/[id]/page.js | 2 - src/app/globals.css | 1 + src/app/perfil/[id]/components/GroupButton.js | 2 +- .../[id]/components/ProfileCollections.js | 2 +- tailwind.config.js | 1 + 14 files changed, 419 insertions(+), 168 deletions(-) create mode 100644 src/app/components/ImageCropper.js create mode 100644 src/app/components/canvasPreview.js diff --git a/package.json b/package.json index 2c60c390..97c0cf07 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.2.1", + "react-image-crop": "^11.0.7", "react-swipeable": "^7.0.1", "tailwind-scrollbar": "^3.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c8e64e9..c95bdf4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-icons: specifier: ^5.2.1 version: 5.3.0(react@18.3.1) + react-image-crop: + specifier: ^11.0.7 + version: 11.0.7(react@18.3.1) react-swipeable: specifier: ^7.0.1 version: 7.0.1(react@18.3.1) @@ -2349,6 +2352,11 @@ packages: peerDependencies: react: '*' + react-image-crop@11.0.7: + resolution: {integrity: sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==} + peerDependencies: + react: '>=16.13.1' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4562,7 +4570,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4586,7 +4594,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4608,7 +4616,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -5432,6 +5440,10 @@ snapshots: dependencies: react: 18.3.1 + react-image-crop@11.0.7(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-is@18.3.1: {} diff --git a/src/app/components/ImageCropper.js b/src/app/components/ImageCropper.js new file mode 100644 index 00000000..acc07c27 --- /dev/null +++ b/src/app/components/ImageCropper.js @@ -0,0 +1,185 @@ +import React, { useState, useRef, useEffect } from "react"; +import ReactCrop, { centerCrop, makeAspectCrop, convertToPixelCrop } from "react-image-crop"; +import "react-image-crop/dist/ReactCrop.css"; +import { canvasPreview } from "./canvasPreview"; +import { getStoredValue, saveToLocalStorage } from "@/app/handlers/localStorageHandler"; +import mecredApi from "@/axiosConfig"; + +export default function ImageCropper({ payloadHeader, type, userId, setChangePhoto }) { + // Estados para gerenciar a foto original, URL da foto, configuração do corte, corte completo e imagem cortada + const [photo, setPhoto] = useState(null); + const [photoURL, setPhotoURL] = useState(null); + const [crop, setCrop] = useState(null); + const [completedCrop, setCompletedCrop] = useState(false); + const [croppedImage, setCroppedImage] = useState(null); + + // Referências para a imagem e o canvas de pré-visualização + const imgRef = useRef(null); + const previewCanvasRef = useRef(null); + + // Recupera tokens de autenticação do armazenamento local + const token = getStoredValue("access_token"); + const client = getStoredValue("client"); + const uid = getStoredValue("uid"); + + // Hook personalizado para aplicar debounce em efeitos + function useDebounceEffect(fn, waitTime, deps = []) { + useEffect(() => { + const t = setTimeout(() => { + fn.apply(undefined, deps); + }, waitTime); + + return () => { + clearTimeout(t); + }; + }, deps); + } + + // Função para fazer upload da foto cortada + const uploadPhoto = async () => { + let payload = new FormData(); + payload.set(payloadHeader, croppedImage); + + await mecredApi.put(`/${type}/${userId}`, payload, { + headers: { + 'access-token': token, + 'token-type': 'Bearer', + 'client': client, + 'uid': uid, + 'Expires': 0 + } + }) + .catch(error => { + setChangePhoto(false); + if (error?.response.status === 500) // TODO: alterar isso quando tivermos um novo backend, pois não deveria retornar 500 toda vez. + console.error(error); // Talvez o backend tenha falhado, mas provavelmente a foto do perfil foi alterada. + }); + + await mecredApi.get(url, { + headers: { + 'access-token': token, + 'token-type': 'Bearer', + 'client': client, + 'uid': uid, + 'Expires': 0 + } + }) + .then(res => { + let userData = JSON.parse(getStoredValue("user_data")); + userData["avatar_file_name"] = res.data.avatar; + saveToLocalStorage("user_data", JSON.stringify(userData)); + }); + }; + + // Manipulador para quando o usuário seleciona uma foto + const handlePhoto = (e) => { + e.preventDefault(); + const file = e.target.files[0]; + if (file) { + setPhoto(file); + setPhotoURL(URL.createObjectURL(file)); + } + }; + + // Função para centralizar o corte com uma proporção específica + function centerAspectCrop(mediaWidth, mediaHeight, aspect) { + return centerCrop( + makeAspectCrop( + { unit: "%", width: 50 }, + aspect, + mediaWidth, + mediaHeight + ), + mediaWidth, + mediaHeight + ); + } + + // Função chamada quando a imagem é carregada + function onImageLoad(e) { + const { width, height } = e.currentTarget; + const newCrop = centerAspectCrop(width, height, 1); + setCrop(newCrop); + setCompletedCrop(convertToPixelCrop(newCrop, width, height)); + } + + // Efeito com debounce para gerar a pré-visualização da imagem cortada + useDebounceEffect( + () => { + if ( + completedCrop?.width && + completedCrop?.height && + imgRef.current && + previewCanvasRef.current + ) { + // Gera a pré-visualização no canvas + canvasPreview( + imgRef.current, + previewCanvasRef.current, + completedCrop, + 1, + 0 + ); + // Converte o conteúdo do canvas em um blob e atualiza o estado + previewCanvasRef.current.toBlob((blob) => { + console.log("meu blob: ", blob); + setCroppedImage(blob); + }); + } + }, + 100, + [completedCrop] + ); + + return ( + <div className="flex flex-col items-center"> + <div className="flex space-x-4 mt-2 mb-5"> + <label className="bg-secondary text-white rounded-lg p-2 cursor-pointer"> + <input type="file" onChange={handlePhoto} className="hidden" /> + Selecionar Nova Foto + </label> + {completedCrop && ( + <button + className="text-sm p-2 text-white border-main rounded-lg font-bold bg-secondary hover:bg-secondary-hover" + onClick={uploadPhoto} + > + Enviar + </button> + )} + </div> + {photoURL && ( + <div className="flex space-x-4"> + <ReactCrop + crop={crop} + onChange={(_, percentCrop) => setCrop(percentCrop)} + onComplete={(c) => setCompletedCrop(c)} + aspect={1} + circularCrop={true} + > + <img + ref={imgRef} + alt="Imagem de Perfil" + src={photoURL} + onLoad={onImageLoad} + style={{ maxWidth: '400px', maxHeight: '400px' }} + /> + </ReactCrop> + {completedCrop && ( + <div className="flex justify-center items-center"> + <canvas + ref={previewCanvasRef} + style={{ + borderRadius: "50%", + border: "2px solid black", + objectFit: "contain", + width: Math.min(completedCrop.width, 200), + height: Math.min(completedCrop.height, 200), + }} + /> + </div> + )} + </div> + )} + </div> + ); +} diff --git a/src/app/components/NavigationBar.js b/src/app/components/NavigationBar.js index 552ba4fd..933e6486 100644 --- a/src/app/components/NavigationBar.js +++ b/src/app/components/NavigationBar.js @@ -60,7 +60,7 @@ export default function NavigationBar({ mobileSearch}) { <> <NeedLoginModal open={needLoginOpen} setOpen={setNeedLoginOpen} /> - <nav className="bg-fundo bg-repeat h-50 outline outline-1 outline-outlineColor text-text-color fixed bottom-0 left-0 w-full z-10"> + <nav className="bg-navbar h-50 outline outline-1 outline-outlineColor text-text-color font-light fixed bottom-0 left-0 w-full z-10"> <ul className="flex justify-between overflow-x-auto no-scrollbar animate-scrollHint"> {navItems.map((item, index) => { const isPublishRoute = item.href === "/publicar"; @@ -74,26 +74,26 @@ export default function NavigationBar({ mobileSearch}) { return ( <li key={index} className="flex w-20 flex-col items-center justify-center p-3"> - <a - href={ - isPublishRoute ? (isLoggedIn() ? "/publicar" : "") : - isPerfilRoute ? (isLoggedIn() ? `/perfil/${id}` : "") : - item.href - } - onClick={ - isPublishRoute || isPerfilRoute ? (e) => handleOpenLoggin(e) : - isSearchButton ? (e) => handleToggleMobileSearch(e) : - undefined - } - className={`text-center rounded-md transition-all ${ - isActive ? "outline outline-1 outline-secondary" : "" - }`} - > - <item.icon className={`cursor-pointer text-3xl ${isActive ? "text-color" : "text-color"}`} /> - <span className={`cursor-pointer text-xs ${isActive ? "text-color" : "text-color"}`}> - {item.label} - </span> - </a> + <a + href={ + isPublishRoute ? (isLoggedIn() ? "/publicar" : "") : + isPerfilRoute ? (isLoggedIn() ? `/perfil/${id}` : "") : + item.href + } + onClick={ + isPublishRoute || isPerfilRoute ? (e) => handleOpenLoggin(e) : + isSearchButton ? (e) => handleToggleMobileSearch(e) : + undefined + } + className={`text-center rounded-md transition-all ${ + isActive ? "font-bold text-black" : "" + }`} + > + <item.icon className={`cursor-pointer text-3xl ${isActive ? "text-black font-bold" : ""}`} /> + <span className={`cursor-pointer text-xs ${isActive ? "text-black font-bold" : ""}`}> + {item.label} + </span> + </a> </li> ); })} diff --git a/src/app/components/Overlay.js b/src/app/components/Overlay.js index ecb1d92b..dfc2316c 100644 --- a/src/app/components/Overlay.js +++ b/src/app/components/Overlay.js @@ -85,7 +85,7 @@ export default function Overlay({ : // Páginas com três colunas e que não precisam de h-full <div - className="h-dvh grid w-full pt-[150px] text-base 2xl:grid-cols-[150px_minmax(0,1fr)_500px] xl:grid-cols-[150px_minmax(0,1fr)_400px] grid-cols-[150px_minmax(0,1fr)]" + className="h-full grid w-full pt-[150px] text-base 2xl:grid-cols-[150px_minmax(0,1fr)_500px] xl:grid-cols-[150px_minmax(0,1fr)_400px] grid-cols-[150px_minmax(0,1fr)]" > <div className="min-h-0"> <SideBar setFilterState={setFilterState} filterState={filterState} /> diff --git a/src/app/components/SideBar.js b/src/app/components/SideBar.js index faebeb52..9fa855da 100644 --- a/src/app/components/SideBar.js +++ b/src/app/components/SideBar.js @@ -4,14 +4,12 @@ import Link from "next/link"; import CollectionsBookmarkIcon from "@mui/icons-material/CollectionsBookmark"; import SubjectIcon from "@mui/icons-material/Subject"; import EmailRoundedIcon from '@mui/icons-material/EmailRounded'; -import LocalLibraryIcon from "@mui/icons-material/LocalLibrary"; import HelpIcon from "@mui/icons-material/Help"; import { useEffect, useState } from "react"; import mecredApi from "@/axiosConfig"; import VerifiedIcon from "@mui/icons-material/Verified"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Person } from "@mui/icons-material"; -import { subjectsAvailable } from "./SubjectsAvailable"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { isLoggedIn } from "../handlers/loginHandler"; import NeedLoginModal from "./needLoginModal"; @@ -134,8 +132,6 @@ export default function SideBar({ setFilterState, filterState }) { const [id, setId] = useState(null); const [logged, setLogged] = useState(false); - const router = useRouter(); - useEffect(() => { if (isLoggedIn()) { let data = JSON.parse(getStoredValue("user_data")); @@ -148,7 +144,7 @@ export default function SideBar({ setFilterState, filterState }) { <> <NeedLoginModal open={needLoginOpen} setOpen={setNeedLoginOpen} /> - <div className="max-md:hidden h-full min-h-0 overflow-y-auto flex flex-col"> + <div className="max-md:hidden h-full min-h-0 overflow-y-auto flex flex-col text-text-color font-light"> <div className="flex flex-col justify-start items-center gap-3 w-full h-full"> {acessoRapido.map((item, index) => { return ( @@ -158,14 +154,14 @@ export default function SideBar({ setFilterState, filterState }) { key={index} alt={item.title} title={item.title} - className={`aspect-square cursor-pointer hover:bg-main-hover focus:bg-main-hover text-center text-xs rounded-lg p-1 w-[55%] max-w-[75px] - ${(page === item.href) || (pathname === item.href) ? "bg-main-hover text-secondary-hover" : "text-gray-color"} + className={`aspect-square cursor-pointer hover:bg-main-hover focus:bg-main-hover text-center rounded-lg w-[55%] max-w-[75px] + ${(page === item.href) || (pathname === item.href) ? "bg-main-hover text-text-filter font-bold" : ""} `} > <item.icon - className={`text-3xl rounded-xl cursor-pointer ${page === item.href ? "text-secondary-hover" : "text-gray-color"}`} + className={`text-3xl rounded-xl cursor-pointer ${page === item.href ? "text-text-filter font-bold" : ""}`} /> - <div className="py-1 text-text-color text-base font-light"> + <div className="py-1 text-text-color text-base"> {item.title} </div> </Link> diff --git a/src/app/components/canvasPreview.js b/src/app/components/canvasPreview.js new file mode 100644 index 00000000..e5308f83 --- /dev/null +++ b/src/app/components/canvasPreview.js @@ -0,0 +1,65 @@ +import { PixelCrop } from 'react-image-crop' + +const TO_RADIANS = Math.PI / 180 + +export function canvasPreview( + image, + canvas, + crop, + scale = 1, + rotate = 0, +) { + const ctx = canvas.getContext('2d') + + if (!ctx) { + throw new Error('No 2d context') + } + + const scaleX = image.naturalWidth / image.width + const scaleY = image.naturalHeight / image.height + // devicePixelRatio slightly increases sharpness on retina devices + // at the expense of slightly slower render times and needing to + // size the image back down if you want to download/upload and be + // true to the images natural size. + const pixelRatio = window.devicePixelRatio + // const pixelRatio = 1 + + canvas.width = Math.floor(crop.width * scaleX * pixelRatio) + canvas.height = Math.floor(crop.height * scaleY * pixelRatio) + + ctx.scale(pixelRatio, pixelRatio) + ctx.imageSmoothingQuality = 'high' + + const cropX = crop.x * scaleX + const cropY = crop.y * scaleY + + const rotateRads = rotate * TO_RADIANS + const centerX = image.naturalWidth / 2 + const centerY = image.naturalHeight / 2 + + ctx.save() + + // 5) Move the crop origin to the canvas origin (0,0) + ctx.translate(-cropX, -cropY) + // 4) Move the origin to the center of the original position + ctx.translate(centerX, centerY) + // 3) Rotate around the origin + ctx.rotate(rotateRads) + // 2) Scale the image + ctx.scale(scale, scale) + // 1) Move the center of the image to the origin (0,0) + ctx.translate(-centerX, -centerY) + ctx.drawImage( + image, + 0, + 0, + image.naturalWidth, + image.naturalHeight, + 0, + 0, + image.naturalWidth, + image.naturalHeight, + ) + + ctx.restore() +} diff --git a/src/app/editar/[id]/components/EditForm.js b/src/app/editar/[id]/components/EditForm.js index ac5cac1a..faaf6d60 100644 --- a/src/app/editar/[id]/components/EditForm.js +++ b/src/app/editar/[id]/components/EditForm.js @@ -4,11 +4,10 @@ import UpdateInfo from "./UpdateInfo"; export default function EditForm({ user }) { return ( - <div className="w-[50%] max-xl:w-full my-10 bg-white rounded-lg shadow-lg"> - <UpdateInfo user={user}/> - <Divider className="mt-5 mx-4"/> - <UpdatePassword user={user} /> - + <div className="w-full bg-white rounded-lg shadow-lg"> + <UpdateInfo user={user}/> + <Divider className="mt-5 mx-4"/> + <UpdatePassword user={user} /> </div> ) } \ No newline at end of file diff --git a/src/app/editar/[id]/components/UpdateInfo.js b/src/app/editar/[id]/components/UpdateInfo.js index b877d4b8..10dcf426 100644 --- a/src/app/editar/[id]/components/UpdateInfo.js +++ b/src/app/editar/[id]/components/UpdateInfo.js @@ -1,9 +1,10 @@ import { Avatar, Button, Modal, TextField } from "@mui/material" import FieldLabel from "@/app/components/FieldLabel" -import { useState } from "react" +import { useState, useMemo } from "react" import mecredApi, { mecredURL } from "@/axiosConfig" import { getStoredValue, saveToLocalStorage } from "@/app/handlers/localStorageHandler" import { useRouter } from "next/navigation" +import ImageCropper from "@/app/components/ImageCropper" export default function UpdateInfo({ user }) { const [name, setName] = useState(user["name"]) @@ -11,9 +12,8 @@ export default function UpdateInfo({ user }) { const [sucessOpen, setSucessOpen] = useState(false) const [notSucessOpen, setNotSucessOpen] = useState(false) const [email, setEmail] = useState(user["email"]) - const [photo, setPhoto] = useState(null) - const [changedPhoto, setChangedPhoto] = useState(false); - const [photoURL, setPhotoURL] = useState(null) + const [changePhoto, setChangePhoto] = useState(false) + const token = getStoredValue("access_token") const client = getStoredValue("client") const uid = getStoredValue("uid") @@ -39,7 +39,7 @@ export default function UpdateInfo({ user }) { ] return colors[id % colors.length]; - } + } const handleSubmit = async (e) => { @@ -58,8 +58,6 @@ export default function UpdateInfo({ user }) { } } - editPhoto() - const url = `/users/${user["id"]}` await mecredApi.put(url, payload, { @@ -71,13 +69,16 @@ export default function UpdateInfo({ user }) { 'Expires': 0 } }) - .then(res => { - setSucessOpen(true); - }) - .catch(() => setNotSucessOpen(true)) + .then(res => { + setSucessOpen(true); + }) + .catch(() => setNotSucessOpen(true)) } + /** + * Handlers de texto + **/ const handleNameChange = (e) => { setName(e.target.value); } @@ -90,53 +91,9 @@ export default function UpdateInfo({ user }) { setEmail(e.target.value); } - const editPhoto = async () => { - const url = `/users/${user["id"]}` - - let payload = new FormData() - payload.set('user[avatar]', photo) - - await mecredApi.put(url, payload, { - headers: { - 'access-token': token, - 'token-type': 'Bearer', - 'client': client, - 'uid': uid, - 'Expires': 0 - } - }) - .catch(error => { - if (error?.response.status === 500) // TODO: change this when we get a new backend, as it shouldn't 500 everytime. - return; // Maybe the backend broke, but probably it worked and the profile picture changed. - - throw error; - }) - - await mecredApi.get(url, { - headers: { - 'access-token': token, - 'token-type': 'Bearer', - 'client': client, - 'uid': uid, - 'Expires': 0 - } - }) - .then(res => { - let userData = JSON.parse(getStoredValue("user_data")); - userData["avatar_file_name"] = res.data.avatar; - saveToLocalStorage("user_data", JSON.stringify(userData)); - }) - - setChangedPhoto(false); - } - - const handlePhoto = (e) => { - e.preventDefault() - setChangedPhoto(true); - setPhoto(e.target.files[0]); - setPhotoURL(URL.createObjectURL(e.target.files[0])); - } - + /* + * Modals + **/ const ModalSucess = ({ open, onClose }) => { return ( <Modal open={open} onClose={onClose} className="grid place-items-center" slotProps={{ @@ -199,82 +156,118 @@ export default function UpdateInfo({ user }) { ) } + const ModalImageCropper = ({ open, onClose }) => { + return ( + <Modal + open={open} + onClose={onClose} + className="grid place-items-center" + slotProps={{ + backdrop: { + sx: { + backgroundColor: "rgba(0, 0, 0, 0.3)", // Ajuste a opacidade conforme necessário + }, + }, + }} + > + <div + className="flex flex-col bg-main p-6 rounded-lg outline outline-1 outline-outlineColor overflow-auto" + style={{ + width: '90%', + maxWidth: '600px', + height: '70vh', + maxHeight: '90vh', + }} + > + <p className="text-xl text-main-text text-center mb-4">Editar Foto de Perfil</p> + + <div className="flex-grow"> + <ImageCropper payloadHeader={"user[avatar]"} type={"users"} userId={user.id} setChangePhoto={setChangePhoto} /> + </div> + + <button + className="text-sm p-2 text-main-text border-main rounded-lg h-9 font-bold bg-main hover:bg-main-hover mt-4" + onClick={onClose} + > + Cancelar + </button> + </div> + </Modal> + ) + } + + return ( <div> <ModalNotSucess open={notSucessOpen} onClose={() => { setNotSucessOpen(false) }} /> <ModalSucess open={sucessOpen} onClose={() => { setSucessOpen(false) }} /> - <form onSubmit={handleSubmit} className=""> - <label className=" text-3xl text-main-text font-bold mt-3 flex justify-center">Editar Perfil</label> - <div className=" flex flex-col mt-4"> - <label className=" text-xl text-main-text font-bold mx-6 ">Foto de perfil</label> - <div className="flex flex-row justify-between my-2 mx-4"> - {photo || user["avatar"] ? - <div className=""> - <img - src={photo ? photoURL : mecredURL + user["avatar"]} - className="w-[150px] h-[150px] object-cover rounded-full" - alt="Editar perfil" - /> - </div> : - <div className={`flex items-center justify-center text-xl font-bold ml-4 text-main rounded-full h-[150px] w-[150px] ${getRandomBg(user["id"])}`} >{user["name"][0]}</div> - - } + <form onSubmit={handleSubmit}> + <label className="text-3xl text-main-text font-bold mt-3 flex justify-center">Editar Perfil</label> + <div className="flex flex-col mt-4 items-center"> + <label className="text-xl text-main-text font-bold mx-6">Foto de perfil</label> + <div className="flex flex-col items-center my-2"> + {user["avatar"] ? ( + <img + src={mecredURL + user["avatar"]} + className="w-[150px] h-[150px] object-cover rounded-full" + alt="Editar perfil" + /> + ) : ( + <div + className={`flex items-center justify-center text-xl font-bold text-main rounded-full h-[150px] w-[150px] ${getRandomBg(user["id"])}`} + > + {user["name"][0]} + </div> + )} + + <button + type="button" + className="bg-secondary text-white rounded-lg mt-2 px-4 py-2" + onClick={() => setChangePhoto(true)} + > + Editar Foto + </button> - {!changedPhoto ? - <div className="flex items-end justify-end"> - <label className="text-main font-bold px-4 py-2 rounded-lg bg-secondary hover:bg-secondary-hover"> - Escolher Imagem - <input type="file" hidden onChange={handlePhoto} /> - </label> - </div> - : - <> - <div className="flex gap-2 items-end justify-end "> - <button className="bg-secondary rounded-lg px-3 py-1 text-white" onClick={editPhoto}> Salvar foto </button> - <button className="bg-secondary rounded-lg px-3 py-1 text-white" onClick={() => { setPhoto(null); setPhotoURL(null); setChangedPhoto(false) }}> Cancelar </button> - </div> - </> - } - </div> + <ModalImageCropper open={changePhoto} onClose={() => setChangePhoto(false)} /> </div> + </div> + <div className="mx-4 mt-4"> + <label className=" text-xl text-main-text font-bold ">Nome Completo *</label> + <TextField + onChange={handleNameChange} + className="w-[100%] mt-2" + defaultValue={user["name"]} + /> + </div> + <div className="mx-4"> + <FieldLabel name="Sobre mim" description="Visível no seu perfil público" /> + <TextField + multiline + rows={3} + onChange={handleDescriptionChange} + helperText={`${description.length}/160`} + error={description.length > 160} + className="w-[100%]" + defaultValue={user["description"]} + /> + </div> + <div className="mx-4 mt-8"> + <label className=" text-xl text-main-text font-bold ">Email *</label> + <TextField + onChange={handleEmailChange} + className="w-[100%] mt-2" + defaultValue={user["email"]} + error={email.match(reEmail) === null} + /> + </div> - <div className="mx-4 mt-4"> - <label className=" text-xl text-main-text font-bold ">Nome Completo *</label> - <TextField - onChange={handleNameChange} - className="w-[100%] mt-2" - defaultValue={user["name"]} - /> - </div> - <div className="mx-4"> - <FieldLabel name="Sobre mim" description="Visível no seu perfil público" /> - <TextField - multiline - rows={3} - onChange={handleDescriptionChange} - helperText={`${description.length}/160`} - error={description.length > 160} - className="w-[100%]" - defaultValue={user["description"]} - /> - </div> - <div className="mx-4 mt-8"> - <label className=" text-xl text-main-text font-bold ">Email *</label> - <TextField - onChange={handleEmailChange} - className="w-[100%] mt-2" - defaultValue={user["email"]} - error={email.match(reEmail) === null} - /> - </div> - - <div className="flex justify-end mt-5 mr-4"> + <div className="flex justify-end mt-5 mr-4"> - <button type="button" onClick={() => router.push(`/perfil/${user["id"]}`)} className="bg-white text-main-text p-2 rounded-lg hover:bg-main-hover mr-1 ">Voltar para perfil</button> - <button type="submit" className="bg-secondary text-white p-2 rounded-lg hover:bg-secondary-hover ml-1 ">Salvar alterações</button> - </div> - </form> + <button type="button" onClick={() => router.push(`/perfil/${user["id"]}`)} className="bg-white text-main-text p-2 rounded-lg hover:bg-main-hover mr-1 ">Voltar para perfil</button> + <button type="submit" className="bg-secondary text-white p-2 rounded-lg hover:bg-secondary-hover ml-1 ">Salvar alterações</button> + </div> + </form> </div> ) } \ No newline at end of file diff --git a/src/app/editar/[id]/page.js b/src/app/editar/[id]/page.js index 191e5862..45d5ab47 100644 --- a/src/app/editar/[id]/page.js +++ b/src/app/editar/[id]/page.js @@ -52,9 +52,7 @@ export default function Edit({ params }) { return ( <Overlay> <Loading loaded={got}> - <div className="flex w-full justify-center items-center"> {userData && Number(params.id) === userData.id ? <EditForm user={userData} /> : <ErrorEdit />} - </div> </Loading> </Overlay> ) diff --git a/src/app/globals.css b/src/app/globals.css index 85622562..1b0c0b34 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -37,6 +37,7 @@ --violet-hover: #410491; --pink: #e62954; --red: #dc2626; + --navbar: #d8e6e6; --red-hover: #a11a1a; --red-publish: #dc2626; --text-color: #6C8080; diff --git a/src/app/perfil/[id]/components/GroupButton.js b/src/app/perfil/[id]/components/GroupButton.js index 125f6ab7..a5cd1e3e 100644 --- a/src/app/perfil/[id]/components/GroupButton.js +++ b/src/app/perfil/[id]/components/GroupButton.js @@ -114,7 +114,7 @@ export default function GroupButton({ profileData, idLogin }) { <div className='flex flex-row gap-4 max-sm:flex-col max-sm:items-center' > {idLogin == profileData["id"] ? <button - className={`text-[16px] rounded-[10px] normal-case h-10 font-bold w-48 text-white bg-orange hover:bg-orange-hover `} + className={`text-[16px] rounded-[10px] normal-case h-10 font-bold w-48 text-white bg-secondary hover:bg-secondary-hover `} alt="Editar meu perfil" onClick={() => router.push(`/editar/${idLogin}`)} > diff --git a/src/app/perfil/[id]/components/ProfileCollections.js b/src/app/perfil/[id]/components/ProfileCollections.js index ddbd94b7..12f50e0a 100644 --- a/src/app/perfil/[id]/components/ProfileCollections.js +++ b/src/app/perfil/[id]/components/ProfileCollections.js @@ -230,7 +230,7 @@ export default function ProfileCollections({ id, idLogin }) { {/* Botão de Excluir */} <button className="p-2 max-md:my-3 text-sm rounded-xl text-black-text font-bold normal-case flex justify-center items-center gap-2 hover:bg-red" - onClick={() => setDeleteOpen(true)} + onClick={() => { setDeleteOpen(true); setColToDelete(item.id);} } aria-label="Excluir" // Texto para leitores de tela > <DeleteOutlinedIcon fontSize="small" /> {/* Ícone de lata de lixo */} diff --git a/tailwind.config.js b/tailwind.config.js index bc484c95..f76abad0 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -41,6 +41,7 @@ module.exports = { "violet-hover": 'var(--violet-hover)', "pink": 'var(--pink)', "red": 'var(--red)', + "navbar": 'var(--navbar)', "red-hover": 'var(--red-hover)', "text-color": 'var(--text-color)', "text-color-click": 'var(--text-color-click)', -- GitLab