diff --git a/package.json b/package.json index 2c60c3906a150a2b5c7692816961e0e442778e65..97c0cf078b90739c88b612e4312f91ef32872283 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 1c8e64e9164a16985e2c647b6fcf5be1c97becad..c95bdf4a96d84f7ab1e44f0f3ef198c2ad90b01c 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 0000000000000000000000000000000000000000..acc07c27f5fad88e3e8f28d84753a9f2713e5b5e --- /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 552ba4fdf4f25ed383f57ea510a7d9e93a35f84c..933e6486bf5d0f89edcedc32fe803f50f6889f81 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 ecb1d92b1ad84e71367cd8dbd78d296025a10d38..dfc2316c00ccd14cdb9ed3ecaf89afd92b15df23 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 faebeb521728f83a480fa647be24b3a2c0dea57b..9fa855da4e63806c4459e6e9c14d081e1f9c5415 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 0000000000000000000000000000000000000000..e5308f83dcd745263cc38cbf178efd20d0854662 --- /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 ac5cac1acb1d7b35b74f3d0f39635f27a8daaa6c..faaf6d609fae907e0b3c4d3930b87817838126a5 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 b877d4b86319949653e9d08e9d74610df59e23e2..10dcf4262b8ba1f046f6eed266ce6f18be6eb327 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 191e5862d804bf66b2e7ef177882e65e938e5475..45d5ab47e8c1f0933e66905a89e3f12be1412e8a 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 856225620a8f8c923672aea50637239a1bc00341..1b0c0b34f06e22a331b06ce4684a3170f41507dc 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 125f6ab7239845f718e5c6d20ede6707f33475a4..a5cd1e3ee61efc925532aaaf796689456f5af92c 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 ddbd94b77fd22158f42de456eb14138a4b35cac7..12f50e0a801763c9039fd158926fd4e74ed83cce 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 bc484c957537afc4aac25000011d2607e038644e..f76abad0be958ca672141987d7eb85730cf2dca7 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)',