From b7ccb248c17e5e9795d053292b4d040cdf9e4a38 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sat, 16 Mar 2024 15:07:05 +0200 Subject: [PATCH 01/13] Add image input field --- .../EditProfile/EditProfile.module.css | 9 ++++--- .../components/EditProfile/EditProfile.tsx | 25 ++++++++++++++++--- .../pages/RootLayout/RootLayout.module.css | 1 + 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/components/EditProfile/EditProfile.module.css b/apps/frontend/src/components/EditProfile/EditProfile.module.css index a5ebb0d3..90a45edc 100644 --- a/apps/frontend/src/components/EditProfile/EditProfile.module.css +++ b/apps/frontend/src/components/EditProfile/EditProfile.module.css @@ -1,5 +1,5 @@ .row { - margin-bottom: 10px; + margin-bottom: 16px; } .profileText { @@ -14,14 +14,15 @@ width: 50%; } +.btn { + width: 100%; +} + @media (max-width: 992px) { .form { width: 100%; } - .btn { - width: 100%; - } .inputField { width: 100%; diff --git a/apps/frontend/src/components/EditProfile/EditProfile.tsx b/apps/frontend/src/components/EditProfile/EditProfile.tsx index 96d86389..fc975126 100644 --- a/apps/frontend/src/components/EditProfile/EditProfile.tsx +++ b/apps/frontend/src/components/EditProfile/EditProfile.tsx @@ -36,6 +36,14 @@ export default function EditProfile({ profile, closeForm }: Props) { closeForm(); }; + const handleImageChange: React.ChangeEventHandler = (event) => { + const target = event.target as HTMLInputElement & { + files: FileList; + }; + + console.log('target', target.files); + }; + return ( <>
@@ -101,13 +109,24 @@ export default function EditProfile({ profile, closeForm }: Props) { maxLength={40}> - - + + +

Image

+ + +
+ + Submit - + Cancel diff --git a/apps/frontend/src/pages/RootLayout/RootLayout.module.css b/apps/frontend/src/pages/RootLayout/RootLayout.module.css index e19e263e..a08664bf 100644 --- a/apps/frontend/src/pages/RootLayout/RootLayout.module.css +++ b/apps/frontend/src/pages/RootLayout/RootLayout.module.css @@ -10,6 +10,7 @@ .mainColumn { margin-left: 4%; + margin-bottom: 2rem; } .sticky { From 7764089b7190b9598b543a8a173cdbf92dc18a49 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sat, 16 Mar 2024 15:14:51 +0200 Subject: [PATCH 02/13] Install cloudindary on backend; create util for image manipulation --- apps/backend/package.json | 1 + package-lock.json | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/apps/backend/package.json b/apps/backend/package.json index 53620726..16f3b2a3 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -23,6 +23,7 @@ "@prisma/client": "^4.14.1", "@types/supertest": "^2.0.12", "bcrypt": "^5.1.0", + "cloudinary": "^2.0.3", "cors": "^2.8.5", "dotenv": "^16.0.3", "dotenv-cli": "^7.1.0", diff --git a/package-lock.json b/package-lock.json index c693a756..513c56d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@prisma/client": "^4.14.1", "@types/supertest": "^2.0.12", "bcrypt": "^5.1.0", + "cloudinary": "^2.0.3", "cors": "^2.8.5", "dotenv": "^16.0.3", "dotenv-cli": "^7.1.0", @@ -6522,6 +6523,18 @@ "node": ">=12" } }, + "node_modules/cloudinary": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.0.3.tgz", + "integrity": "sha512-2JPxAUuV4iHwiW4ATSOZvii6+BhhKI9+9KscgUkxJPKa6V6wOnZJHlYyovBGrrIbIgEdmGSZgqEsLfD0wWBhBg==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", From 18fc10137b7ce1624463c36896f2437e2987b688 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sat, 16 Mar 2024 15:46:35 +0200 Subject: [PATCH 03/13] Install cloudinary on frontend; config with React --- apps/frontend/package.json | 2 + .../src/pages/ProfilePage/ProfilePage.tsx | 6 +- package-lock.json | 71 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 3bf7e1bc..ce5d74ea 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,6 +6,8 @@ "license": "ISC", "main": "index.ts", "dependencies": { + "@cloudinary/react": "^1.13.0", + "@cloudinary/url-gen": "^1.19.0", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", diff --git a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx index b15528ff..02f71195 100644 --- a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx +++ b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx @@ -8,6 +8,7 @@ import { getStoredUser } from '../../utils/auth'; import usersServices from '../../services/users'; import EditProfile from '../../components/EditProfile/EditProfile'; import ProfileInfo from '../../components/ProfileInfo/ProfileInfo'; +import CloudImg from '../../components/CloudImg/CouldImg'; export async function loader({ params }: LoaderFunctionArgs) { const userId = params.id; @@ -25,7 +26,7 @@ export async function action({ params, request }: ActionFunctionArgs) { return response; } -const profileImage = require('./profile-picture.png'); +// const profileImage = require('./profile-picture.png'); export default function ProfilePage() { const loggedUser = getStoredUser(); @@ -55,7 +56,8 @@ export default function ProfilePage() { - Profile + {/* Profile */} +

@{profileData.username}

diff --git a/package-lock.json b/package-lock.json index 513c56d9..1a094e35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@cloudinary/react": "^1.13.0", + "@cloudinary/url-gen": "^1.19.0", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", @@ -2085,6 +2087,54 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@cloudinary/html": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@cloudinary/html/-/html-1.13.0.tgz", + "integrity": "sha512-kOE54EbAyxSH4k3Z9HzYDSlsfpXSDxckAdyyNfxsnC23u+wfxY0Gt8yBG0s3tiqdTkL6IDd1Momsn/OlYStObg==", + "dependencies": { + "@types/lodash.clonedeep": "^4.5.6", + "@types/lodash.debounce": "^4.0.6", + "@types/node": "^14.14.10", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "typescript": "^4.1.2" + } + }, + "node_modules/@cloudinary/html/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@cloudinary/react": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@cloudinary/react/-/react-1.13.0.tgz", + "integrity": "sha512-S+Wmdmg4+pjWWlRLUcaHlQ0d59njecja6wGGWGJwVsQioBF/E/KUxNl09Q6NemXuJJAVW/hSW9kT/diJmI7eQQ==", + "dependencies": { + "@cloudinary/html": "^1.13.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@cloudinary/transformation-builder-sdk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@cloudinary/transformation-builder-sdk/-/transformation-builder-sdk-1.13.1.tgz", + "integrity": "sha512-APEOPRoJZF4Z0zOLOi3UVbmb2g3NnhsYlGRhZLL4KadYMEisp26IrPhhw8aP3Luq2QI1KAJBi4aXN23fipDiiQ==", + "dependencies": { + "@cloudinary/url-gen": "^1.7.0" + } + }, + "node_modules/@cloudinary/url-gen": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@cloudinary/url-gen/-/url-gen-1.19.0.tgz", + "integrity": "sha512-3WSqYAMEe8lcYMFmkNPhOfv6Ql76mpf8O7YEC+CCaoIYrfBubzzeF8yENCaZK9tFJEHvMepI+uWWfilNrz48XA==", + "dependencies": { + "@cloudinary/transformation-builder-sdk": "^1.13.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4739,6 +4789,27 @@ "@types/geojson": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", From 54c9f5598859f11b87e4470208a65a8b3287490f Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sat, 16 Mar 2024 16:49:19 +0200 Subject: [PATCH 04/13] Upload picture from edit profile form --- .../components/EditProfile/EditProfile.tsx | 29 +++++++++++++++---- apps/frontend/src/utils/auth.ts | 6 ++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/components/EditProfile/EditProfile.tsx b/apps/frontend/src/components/EditProfile/EditProfile.tsx index fc975126..c9969a21 100644 --- a/apps/frontend/src/components/EditProfile/EditProfile.tsx +++ b/apps/frontend/src/components/EditProfile/EditProfile.tsx @@ -12,6 +12,7 @@ type Props = { }; export default function EditProfile({ profile, closeForm }: Props) { + const [profPic, setProfPic] = useState(undefined); const [formInput, setFormInput] = useState({ first_name: profile.first_name || '', last_name: profile.last_name || '', @@ -22,14 +23,30 @@ export default function EditProfile({ profile, closeForm }: Props) { const submit = useSubmit(); - const handleSubmit = (event: FormEvent) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - const form: UpdateUserInput = { ...formInput }; - if (!form.dob) { - delete form.dob; + const formDeets: UpdateUserInput = { ...formInput }; + if (!formDeets.dob) { + delete formDeets.dob; } - submit(form, { + if (profPic) { + const URL = 'https://api.cloudinary.com/v1_1/dwlk6urra/image/upload'; + const cloudFormData = new FormData(); + cloudFormData.append('file', profPic); + cloudFormData.append('upload_preset', 'prof_pic') + cloudFormData.append('api_key', process.env.REACT_APP_CLOUDINARY_API_KEY || ''); + + const response = await fetch(URL, { + method: 'post', + body: cloudFormData, + }).then(res => res.json()) + .catch(console.error); + + console.log(response); + } + + submit(formDeets, { method: 'put', action: `/users/${profile.id}`, }); @@ -41,7 +58,7 @@ export default function EditProfile({ profile, closeForm }: Props) { files: FileList; }; - console.log('target', target.files); + setProfPic(target.files[0]); }; return ( diff --git a/apps/frontend/src/utils/auth.ts b/apps/frontend/src/utils/auth.ts index 657cd1e5..1a9107e5 100644 --- a/apps/frontend/src/utils/auth.ts +++ b/apps/frontend/src/utils/auth.ts @@ -48,10 +48,8 @@ export function checkAuthLoader() { const decodedToken = jwtDecode(user.token); - const isTokenExpired = decodedToken.exp ? Date.now() >= decodedToken.exp * 1000 : null; - console.log(isTokenExpired); - - // if (isTokenExpired) return redirect('/login'); + const isTokenExpired = decodedToken.exp ? Date.now() >= decodedToken.exp * 1000 : null; + if (isTokenExpired) return redirect('/login'); return null; } From 7223addde8e85943bb71c2cbc4df39ae053cfb88 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sun, 17 Mar 2024 12:39:18 +0200 Subject: [PATCH 05/13] Upload user img from backend; fix dob bug. Install package to accept FormData on backend. Send user deets as formData along with image if submitted. Upload image to cloudinary and send back URL. Fix date bug by sending undefined to db for empty date input --- apps/backend/package.json | 1 + apps/backend/prisma/schema.prisma | 1 + apps/backend/src/controllers/users.ts | 19 +- apps/backend/src/services/userServices.ts | 71 ++++--- apps/backend/src/types.ts | 5 +- .../components/EditProfile/EditProfile.tsx | 58 +++--- .../src/pages/ProfilePage/ProfilePage.tsx | 7 +- apps/frontend/src/services/users.ts | 21 +- apps/frontend/src/types.ts | 12 ++ package-lock.json | 189 ++++++++++++++++++ package.json | 1 + 11 files changed, 328 insertions(+), 57 deletions(-) diff --git a/apps/backend/package.json b/apps/backend/package.json index 16f3b2a3..6476863b 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -29,6 +29,7 @@ "dotenv-cli": "^7.1.0", "express": "^4.17.3", "express-async-errors": "^3.1.1", + "express-form-data": "^2.0.23", "jsonwebtoken": "^9.0.0", "pg": "^8.10.0" }, diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 243f1e3c..91cf554f 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -65,6 +65,7 @@ model User { first_name String? @db.VarChar(25) last_name String? @db.VarChar(25) dob DateTime? @db.Date + image_url String? @db.VarChar(400) gender_id Int? bio String? @db.VarChar(500) adminOf Neighborhood[] @relation("admin") diff --git a/apps/backend/src/controllers/users.ts b/apps/backend/src/controllers/users.ts index 63144256..fefb3f8a 100644 --- a/apps/backend/src/controllers/users.ts +++ b/apps/backend/src/controllers/users.ts @@ -1,4 +1,5 @@ import express, { Request, Response } from 'express'; +import * as formData from 'express-form-data'; import catchError from '../utils/catchError'; import userServices from '../services/userServices'; import middleware from '../utils/middleware'; @@ -7,6 +8,15 @@ import { UserWithoutPasswordHash, RequestWithAuthentication } from '../types'; const usersRouter = express.Router(); +// parse data with connect-multiparty. +usersRouter.use(formData.parse()); +// delete from the request all empty files (size == 0) +usersRouter.use(formData.format()); +// change the file objects to fs.ReadStream +usersRouter.use(formData.stream()); +// union the body and the files +usersRouter.use(formData.union()); + usersRouter.get( '/', catchError(async (_req: Request, res: Response) => { @@ -54,7 +64,14 @@ usersRouter.put( if (!(userId === Number(req.loggedUserId))) { return res.status(401).json('Logged user is not the owner of this profile'); } - const updatedUser = await userServices.updateUser(req.body, userId); + + if ('image_url' in req.body && 'path' in req.body.image_url) { + req.body.image_url = req.body.image_url.path; + } else { + req.body.image_url = undefined; + } + + const updatedUser = await userServices.updateUser(req.body, userId); return res.status(200).json(updatedUser); }), ); diff --git a/apps/backend/src/services/userServices.ts b/apps/backend/src/services/userServices.ts index 5dde5222..cc270576 100644 --- a/apps/backend/src/services/userServices.ts +++ b/apps/backend/src/services/userServices.ts @@ -1,8 +1,9 @@ import bcrypt from 'bcrypt'; -import { User } from '@prisma/client'; -import { CreateUserData, UpdateUserData, UpdateUserInput, UserWithoutId, UserWithoutPasswordHash } from '../types'; +import { Prisma, User } from '@prisma/client'; import prismaClient from '../../prismaClient'; +import { CreateUserData, UpdateUserData, UpdateUserInput, UserWithoutId, UserWithoutPasswordHash } from '../types'; import { isObject,stringIsValidDate } from '../utils/helpers'; +import { uploadImage } from './imageServices'; const USER_FIELDS_WITHOUT_PASSWORD_HASH = { id: true, @@ -13,6 +14,7 @@ const USER_FIELDS_WITHOUT_PASSWORD_HASH = { dob: true, gender_id: true, bio: true, + image_url: true, neighborhoods: true, requests: { include: { @@ -177,6 +179,7 @@ const generateUserDataWithoutId = async (createUserData: CreateUserData) dob: null, gender_id: null, bio: null, + image_url: createUserData.image_url || null }; return userData; @@ -215,7 +218,7 @@ const createUser = async (userData: CreateUserData): Promise { - const VALID_PROPS = ['first_name', 'last_name', 'email', 'dob', 'bio']; + const VALID_PROPS = ['first_name', 'last_name', 'email', 'dob', 'bio', 'image_url']; const props = Object.keys(obj); if (props.some((prop) => !VALID_PROPS.includes(prop))) return false; @@ -224,6 +227,7 @@ const isUpdateProfileData = (obj: object): obj is UpdateUserInput => { if ('dob' in obj && typeof obj.dob !== 'string') return false; if ('email' in obj && typeof obj.email !== 'string') return false; if ('bio' in obj && typeof obj.bio !== 'string') return false; + // if ('image_url' in obj && (typeof obj.image_url !== 'string' || obj.image_url instanceof File)) return true; }; @@ -244,33 +248,52 @@ const updateUser = async (body: unknown, userId: number): Promise) => { event.preventDefault(); @@ -30,26 +32,36 @@ export default function EditProfile({ profile, closeForm }: Props) { delete formDeets.dob; } - if (profPic) { - const URL = 'https://api.cloudinary.com/v1_1/dwlk6urra/image/upload'; - const cloudFormData = new FormData(); - cloudFormData.append('file', profPic); - cloudFormData.append('upload_preset', 'prof_pic') - cloudFormData.append('api_key', process.env.REACT_APP_CLOUDINARY_API_KEY || ''); - - const response = await fetch(URL, { - method: 'post', - body: cloudFormData, - }).then(res => res.json()) - .catch(console.error); - - console.log(response); - } - - submit(formDeets, { - method: 'put', - action: `/users/${profile.id}`, - }); + const formData = new FormData(); + Object.entries(formInput).forEach(([key, value]) => formData.append(key, value)); + if (profPic) formData.append('image_url', profPic); + + // if (profPic) { + // const URL = 'https://api.cloudinary.com/v1_1/dwlk6urra/image/upload'; + // const cloudFormData = new FormData(); + // cloudFormData.append('file', profPic); + // cloudFormData.append('upload_preset', 'prof_pic'); + // cloudFormData.append('overwrite', 'true'); + // cloudFormData.append('public_id', `${profile.username}:${profile.id}`); + // cloudFormData.append('api_key', process.env.REACT_APP_CLOUDINARY_API_KEY || ''); + + // const response = await fetch(URL, { + // method: 'post', + // body: cloudFormData, + // }).then(res => res.json()) + // .catch(console.error); + + // console.log(response); + // // secure_url + // } + + const res = await userServices.updateProfile(formData, profile.id); + console.log(res); + + // submit(formData, { + // method: 'put', + // action: `/users/${profile.id}`, + // }); closeForm(); }; diff --git a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx index 02f71195..008699d3 100644 --- a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx +++ b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx @@ -1,6 +1,6 @@ import { ActionFunctionArgs, LoaderFunctionArgs, useLoaderData } from 'react-router'; import { useState } from 'react'; -import { UpdateUserInput, UserWithRelatedData } from '@neighborhood/backend/src/types'; +import { UserWithRelatedData } from '@neighborhood/backend/src/types'; import { Container, Row, Col } from 'react-bootstrap'; import CustomBtn from '../../components/CustomBtn/CustomBtn'; import styles from './ProfilePage.module.css'; @@ -9,6 +9,7 @@ import usersServices from '../../services/users'; import EditProfile from '../../components/EditProfile/EditProfile'; import ProfileInfo from '../../components/ProfileInfo/ProfileInfo'; import CloudImg from '../../components/CloudImg/CouldImg'; +import { UpdateUserInput } from '../../types'; export async function loader({ params }: LoaderFunctionArgs) { const userId = params.id; @@ -19,6 +20,10 @@ export async function loader({ params }: LoaderFunctionArgs) { export async function action({ params, request }: ActionFunctionArgs) { const formData = await request.formData(); const profileId = Number(params.id); + // eslint-disable-next-line no-restricted-syntax + for (const prop of formData) { + console.log(prop); + } const profileData = Object.fromEntries(formData) as unknown as UpdateUserInput; diff --git a/apps/frontend/src/services/users.ts b/apps/frontend/src/services/users.ts index 02c09610..53618d55 100644 --- a/apps/frontend/src/services/users.ts +++ b/apps/frontend/src/services/users.ts @@ -1,6 +1,7 @@ -import axios from 'axios'; -import { UserWithRelatedData, UpdateUserInput, UserWithoutPasswordHash } from '@neighborhood/backend/src/types'; +import axios, { AxiosError } from 'axios'; +import { UserWithRelatedData, UserWithoutPasswordHash } from '@neighborhood/backend/src/types'; import { getStoredUser } from '../utils/auth'; +import { ErrorObj, UpdateUserInput } from '../types'; const baseURL = '/api/users'; @@ -17,17 +18,23 @@ async function getUserData(id: number): Promise { return response.data; } -async function updateProfile(updateProfileData: UpdateUserInput, userId: number): Promise { +async function updateProfile(updateProfileData: UpdateUserInput | FormData, userId: number): Promise { const headers: { authorization?: string } = {}; const user = getStoredUser(); if (user) { headers.authorization = `Bearer ${user.token}`; } - - const response = await axios.put(`${baseURL}/${userId}`, updateProfileData, { headers }); - - return response.data; + try { + const response = await axios.put(`${baseURL}/${userId}`, updateProfileData, { headers }); + return response.data; + } catch (error) { + if (error instanceof AxiosError) { + return error.response?.data + } + + throw error; + } } export default { getUserData, updateProfile }; diff --git a/apps/frontend/src/types.ts b/apps/frontend/src/types.ts index 48258ebe..f185184f 100644 --- a/apps/frontend/src/types.ts +++ b/apps/frontend/src/types.ts @@ -47,6 +47,18 @@ export type SignUpData = CreateUserData; export type UserInfo = Omit; +/** + * format of the data sent to `PUT /user/:id` to edit user + */ +export type UpdateUserInput = { + first_name: string; + last_name: string; + bio: string; + email: string; + dob?: string; + image_url?: File; +}; + export type SingleNeighborhoodFormIntent = | 'create-request' | 'join-neighborhood' diff --git a/package-lock.json b/package-lock.json index 1a094e35..1c37df34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "react-bootstrap-typeahead": "^6.3.2" }, "devDependencies": { + "@types/express-form-data": "^2.0.5", "eslint-config-prettier": "^9.0.0", "prettier": "3.0.3" } @@ -33,6 +34,7 @@ "dotenv-cli": "^7.1.0", "express": "^4.17.3", "express-async-errors": "^3.1.1", + "express-form-data": "^2.0.23", "jsonwebtoken": "^9.0.0", "pg": "^8.10.0" }, @@ -4655,6 +4657,16 @@ "@types/serve-static": "*" } }, + "node_modules/@types/express-form-data": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/express-form-data/-/express-form-data-2.0.5.tgz", + "integrity": "sha512-zn1Cvy/scDOgV4j5pUKxsPQQVIX5vAGL6xjLWteSApd3HAmmfbdeZ3WMbFAks0kIaAxbSIT79W9GB4AVkS6fOA==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/multiparty": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "4.17.35", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", @@ -4815,6 +4827,15 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "node_modules/@types/multiparty": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.36.tgz", + "integrity": "sha512-DNqs3xYFMLgQfYrYuuleox1wzpUJxOgR8+WQ99DPu1B6zLT+sCMMsWfqEFTThPrI3109q81sChUKg/QM/ZUPiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "16.18.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.33.tgz", @@ -6859,6 +6880,84 @@ "node": ">=0.8" } }, + "node_modules/connect-multiparty": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/connect-multiparty/-/connect-multiparty-2.2.0.tgz", + "integrity": "sha512-zKcpA7cuXGEhuw9Pz7JmVCFmp85jzGLGm/iiagXTwyEAJp4ypLPtRS/V4IGuGb9KjjrgHBs6P/gDCpZHnFzksA==", + "dependencies": { + "http-errors": "~1.7.0", + "multiparty": "~4.2.1", + "on-finished": "~2.3.0", + "qs": "~6.5.2", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/connect-multiparty/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/connect-multiparty/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/connect-multiparty/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect-multiparty/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/connect-multiparty/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/connect-multiparty/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/connect-multiparty/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -9033,6 +9132,33 @@ "express": "^4.16.2" } }, + "node_modules/express-form-data": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/express-form-data/-/express-form-data-2.0.23.tgz", + "integrity": "sha512-Efm2t+xL6vEqUPs4kAyBY9xG/8LGpbSVnnFLZw6HgdxHzSRlg2/sNlA2UdMlcIKMH+LDOn7NKXUDGJK125F4rw==", + "dependencies": { + "connect-multiparty": "^2.2.0", + "fs-extra": "^9.1.0", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": ">=10.7.0" + } + }, + "node_modules/express-form-data/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -13084,6 +13210,50 @@ "multicast-dns": "cli.js" } }, + "node_modules/multiparty": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz", + "integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==", + "dependencies": { + "http-errors": "~1.8.1", + "safe-buffer": "5.2.1", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/multiparty/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multiparty/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multiparty/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15411,6 +15581,14 @@ "performance-now": "^2.1.0" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -19171,6 +19349,17 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 29bfa878..6d786919 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react-bootstrap-typeahead": "^6.3.2" }, "devDependencies": { + "@types/express-form-data": "^2.0.5", "eslint-config-prettier": "^9.0.0", "prettier": "3.0.3" } From f78bc18d826b37b98ea8da83fd88538a5dc8e4dc Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sun, 17 Mar 2024 13:09:03 +0200 Subject: [PATCH 06/13] Add image_url field to db; show user image or placeholder on their profile --- .../migration.sql | 2 + apps/backend/src/controllers/images.ts | 19 +++++ apps/backend/src/services/imageServices.ts | 67 +++++++++++++++ apps/backend/src/utils/cloudinary.ts | 76 ++++++++++++++++++ apps/frontend/src/assets/icons/user_icon.png | Bin 0 -> 12119 bytes .../components/CloudImg/CouldImg.module.css | 0 .../src/components/CloudImg/CouldImg.tsx | 26 ++++++ .../src/pages/ProfilePage/ProfilePage.tsx | 20 ++--- 8 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 apps/backend/prisma/migrations/20240316170250_add_image_url_to_user_table/migration.sql create mode 100644 apps/backend/src/controllers/images.ts create mode 100644 apps/backend/src/services/imageServices.ts create mode 100644 apps/backend/src/utils/cloudinary.ts create mode 100644 apps/frontend/src/assets/icons/user_icon.png create mode 100644 apps/frontend/src/components/CloudImg/CouldImg.module.css create mode 100644 apps/frontend/src/components/CloudImg/CouldImg.tsx diff --git a/apps/backend/prisma/migrations/20240316170250_add_image_url_to_user_table/migration.sql b/apps/backend/prisma/migrations/20240316170250_add_image_url_to_user_table/migration.sql new file mode 100644 index 00000000..03764f43 --- /dev/null +++ b/apps/backend/prisma/migrations/20240316170250_add_image_url_to_user_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "image_url" VARCHAR(400); diff --git a/apps/backend/src/controllers/images.ts b/apps/backend/src/controllers/images.ts new file mode 100644 index 00000000..622c4ed1 --- /dev/null +++ b/apps/backend/src/controllers/images.ts @@ -0,0 +1,19 @@ +// import express, { Response } from 'express'; +// import catchError from '../utils/catchError'; +// import middleware from '../utils/middleware'; + +// const imageRouter = express.Router(); + +// imageRouter.post( +// '/upload', +// middleware.isUserLoggedIn, +// catchError(async (request: RequestWithAuthentication, response: Response) => { +// const userIsLoggedIn = typeof request.loggedUserId === 'number'; + + + +// return response.status(200)); +// }), +// ); + +// export default imageRouter; diff --git a/apps/backend/src/services/imageServices.ts b/apps/backend/src/services/imageServices.ts new file mode 100644 index 00000000..7737c4d9 --- /dev/null +++ b/apps/backend/src/services/imageServices.ts @@ -0,0 +1,67 @@ +// Require the cloudinary library +const cloudinary = require('cloudinary').v2; + +// Return "https" URLs by setting secure: true +cloudinary.config({ + secure: true, +}); + +// Log the configuration +console.log(cloudinary.config()); + +// Uploads an image file // +const uploadImage = async (imagePath: File | string, publicId: string): Promise => { + // Allow overwriting the asset with new versions + const options = { + public_id: publicId, + overwrite: true, + }; + + try { + const result = await cloudinary.uploader.upload(imagePath, options); + console.log(result); + return result.public_id; + } catch (error) { + console.error(error); + } +}; + +// Gets details of an uploaded image // +const getAssetInfo = async (publicId: number) => { + // Return colors in the response + const options = { + colors: true, + }; + + try { + // Get details about the asset + const result = await cloudinary.api.resource(publicId, options); + console.log(result); + return result.colors; + } catch (error) { + console.error(error); + } +}; + +// Creates an HTML image tag with a transformation that +// results in a circular thumbnail crop of the image +// focused on the faces, applying an outline of the +// first color, and setting a background of the second color. +const createImageTag = (publicId: number, ...colors: string[]) => { + // Set the effect color and background color + const [effectColor, backgroundColor] = colors; + + // Create an image tag with transformations applied to the src URL + const imageTag = cloudinary.image(publicId, { + transformation: [ + { width: 250, height: 250, gravity: 'faces', crop: 'thumb' }, + { radius: 'max' }, + { effect: 'outline:10', color: effectColor }, + { background: backgroundColor }, + ], + }); + + return imageTag; +}; + +export { uploadImage, getAssetInfo, createImageTag }; diff --git a/apps/backend/src/utils/cloudinary.ts b/apps/backend/src/utils/cloudinary.ts new file mode 100644 index 00000000..da7a726e --- /dev/null +++ b/apps/backend/src/utils/cloudinary.ts @@ -0,0 +1,76 @@ +// // Require the cloudinary library +// const cloudinary = require('cloudinary').v2; + +// // Return "https" URLs by setting secure: true +// cloudinary.config({ +// secure: true, +// }); + +// // Log the configuration +// console.log(cloudinary.config()); + +// // Uploads an image file // +// const uploadImage = async (imagePath: File | string, publicId: number): Promise => { +// // Allow overwriting the asset with new versions +// const options = { +// public_id: publicId, +// overwrite: true, +// }; + +// try { +// // Upload the image +// const result = await cloudinary.uploader.upload(imagePath, options); +// console.log(result); +// return result.secure_url; +// } catch (error) { +// console.error(error); +// } +// }; + + +// // Gets details of an uploaded image // +// const getAssetInfo = async (publicId: number) => { + +// // Return colors in the response +// const options = { +// colors: true, +// }; + +// try { +// // Get details about the asset +// const result = await cloudinary.api.resource(publicId, options); +// console.log(result); +// return result.colors; +// } catch (error) { +// console.error(error); +// } +// }; + + +// // Creates an HTML image tag with a transformation that +// // results in a circular thumbnail crop of the image +// // focused on the faces, applying an outline of the +// // first color, and setting a background of the second color. +// const createImageTag = (publicId: number, ...colors: string[]) => { + +// // Set the effect color and background color +// const [effectColor, backgroundColor] = colors; + +// // Create an image tag with transformations applied to the src URL +// const imageTag = cloudinary.image(publicId, { +// transformation: [ +// { width: 250, height: 250, gravity: 'faces', crop: 'thumb' }, +// { radius: 'max' }, +// { effect: 'outline:10', color: effectColor }, +// { background: backgroundColor }, +// ], +// }); + +// return imageTag; +// }; + +// export { +// uploadImage, +// getAssetInfo, +// createImageTag +// } \ No newline at end of file diff --git a/apps/frontend/src/assets/icons/user_icon.png b/apps/frontend/src/assets/icons/user_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..42899c427fbe602722328a843b21b630673a8c3a GIT binary patch literal 12119 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 V()8lI`0v+fIVoky-{K|#{{v7=Tsi;% literal 0 HcmV?d00001 diff --git a/apps/frontend/src/components/CloudImg/CouldImg.module.css b/apps/frontend/src/components/CloudImg/CouldImg.module.css new file mode 100644 index 00000000..e69de29b diff --git a/apps/frontend/src/components/CloudImg/CouldImg.tsx b/apps/frontend/src/components/CloudImg/CouldImg.tsx new file mode 100644 index 00000000..cb9bd54a --- /dev/null +++ b/apps/frontend/src/components/CloudImg/CouldImg.tsx @@ -0,0 +1,26 @@ +import { Cloudinary } from '@cloudinary/url-gen'; +import { AdvancedImage } from '@cloudinary/react'; +import { fill } from '@cloudinary/url-gen/actions/resize'; + +// import styles from './CouldImg.module.css'; + +export default function CloudImg({ src, className }: { src: string; className: string}) { + const cld = new Cloudinary({ + cloud: { + cloudName: 'dwlk6urra', + }, + }); + + // Instantiate a CloudinaryImage object for the image with the public ID, 'docs/models'. + const myImage = cld.image(src); + + // Resize to 250 x 250 pixels using the 'fill' crop mode. + myImage.resize(fill().width(250).height(250)); + + // Render the image in a React component. + return ( +
+ +
+ ); + } diff --git a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx index 008699d3..21639237 100644 --- a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx +++ b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx @@ -1,7 +1,7 @@ import { ActionFunctionArgs, LoaderFunctionArgs, useLoaderData } from 'react-router'; import { useState } from 'react'; import { UserWithRelatedData } from '@neighborhood/backend/src/types'; -import { Container, Row, Col } from 'react-bootstrap'; +import { Container, Row, Col, Image } from 'react-bootstrap'; import CustomBtn from '../../components/CustomBtn/CustomBtn'; import styles from './ProfilePage.module.css'; import { getStoredUser } from '../../utils/auth'; @@ -20,10 +20,6 @@ export async function loader({ params }: LoaderFunctionArgs) { export async function action({ params, request }: ActionFunctionArgs) { const formData = await request.formData(); const profileId = Number(params.id); - // eslint-disable-next-line no-restricted-syntax - for (const prop of formData) { - console.log(prop); - } const profileData = Object.fromEntries(formData) as unknown as UpdateUserInput; @@ -31,7 +27,7 @@ export async function action({ params, request }: ActionFunctionArgs) { return response; } -// const profileImage = require('./profile-picture.png'); +const profileImgPlaceholder = require('../../assets/icons/user_icon.png'); export default function ProfilePage() { const loggedUser = getStoredUser(); @@ -56,14 +52,18 @@ export default function ProfilePage() { : null; const showUpdateButton = isUserAdmin && !edit; + const userImg = profileData.image_url ? ( + + ) : ( + + ); + console.log(profileData); + return ( - - {/* Profile */} - - + {userImg}

@{profileData.username}

{nameOfUser &&

{nameOfUser}

} From 8709332d484cf552e32b03ffea33c97945648cbb Mon Sep 17 00:00:00 2001 From: nina-dk Date: Sun, 17 Mar 2024 14:42:01 +0200 Subject: [PATCH 07/13] Update subscriber info when user data is updated --- apps/backend/src/controllers/users.ts | 1 + apps/backend/src/services/imageServices.ts | 3 + .../src/services/notificationServices.ts | 55 +++++++++- apps/backend/src/services/userServices.ts | 103 +++++++++++------- 4 files changed, 123 insertions(+), 39 deletions(-) diff --git a/apps/backend/src/controllers/users.ts b/apps/backend/src/controllers/users.ts index fefb3f8a..60004a3a 100644 --- a/apps/backend/src/controllers/users.ts +++ b/apps/backend/src/controllers/users.ts @@ -49,6 +49,7 @@ usersRouter.post( newUser.username, newUser.first_name || '', newUser.last_name || '', + newUser.image_url || '', ); return res.status(201).json(newUser); diff --git a/apps/backend/src/services/imageServices.ts b/apps/backend/src/services/imageServices.ts index 7737c4d9..75f4487e 100644 --- a/apps/backend/src/services/imageServices.ts +++ b/apps/backend/src/services/imageServices.ts @@ -1,6 +1,9 @@ // Require the cloudinary library const cloudinary = require('cloudinary').v2; +export const URL = + `https://res.cloudinary.com/${process.env.CLOUD_NAME}/image/upload/v1710605561/`; + // Return "https" URLs by setting secure: true cloudinary.config({ secure: true, diff --git a/apps/backend/src/services/notificationServices.ts b/apps/backend/src/services/notificationServices.ts index 24710d5c..0d109709 100644 --- a/apps/backend/src/services/notificationServices.ts +++ b/apps/backend/src/services/notificationServices.ts @@ -84,20 +84,24 @@ export async function getTopics(numOfTopics: number): Promise { /** * Creates a new subscriber to receive notifications - * @param id + * @param id to use as subscriberId + * @param username * @param firstName * @param lastName + * @param imageURL (optional) to use as the user's avatar */ export async function createSubscriber( id: string, username: string, firstName: string, lastName: string, + imageURL?: string, ) { try { await novu.subscribers.identify(id, { firstName, lastName, + avatar: imageURL, data: { username, }, @@ -122,6 +126,42 @@ export async function getSubscriber(subscriberId: string) { } } +/** + * Updates a subscriber's info + * @param subscriberId + */ +export async function updateSubcriber({ + subscriberId, + firstName, + lastName, + email, + imageUrl, +}: { + subscriberId: string; + firstName: string; + lastName: string; + email: string; + imageUrl?: string; +}) { + try { + // If none of the values were updated ('' or undefined), return immediately + if ([firstName, lastName, email, imageUrl].every((val) => !val)) return; + + // eslint-disable-next-line prefer-rest-params + console.log(arguments); + + const res = await novu.subscribers.update(subscriberId, { + firstName, + lastName, + email, + avatar: imageUrl, + }); + console.log('novu res', res.data.data); + } catch (error) { + console.error(error); + } +} + /** * Retrieves all subscribers. * @returns all subcribers or a custom error object if an error occurs @@ -202,6 +242,11 @@ export const triggers = { username, }: JoinNeighborhoodArgs) { try { + /* + * Checks if there is an identical notification in the recent history. + * Note that this implementation only checks the first page of results + * and not ALL the admin's notifications. + */ const notifications = await getSubscriberNotifications(adminId); const identicalNotification = notifications.some( @@ -214,10 +259,18 @@ export const triggers = { if (identicalNotification) return; + const subscriberInfo = (await getSubscriber(userId)).data; + console.log({subscriberInfo}); + await novu.trigger('join-neighborhood', { to: { subscriberId: adminId, }, + actor: { + subscriberId: userId, + // Maybe passing the id is enough to grab the avatar. Check + avatar: subscriberInfo.avatar + }, payload: { neighborhoodId, neighborhoodName, diff --git a/apps/backend/src/services/userServices.ts b/apps/backend/src/services/userServices.ts index cc270576..af46bdd3 100644 --- a/apps/backend/src/services/userServices.ts +++ b/apps/backend/src/services/userServices.ts @@ -1,9 +1,16 @@ import bcrypt from 'bcrypt'; import { Prisma, User } from '@prisma/client'; import prismaClient from '../../prismaClient'; -import { CreateUserData, UpdateUserData, UpdateUserInput, UserWithoutId, UserWithoutPasswordHash } from '../types'; -import { isObject,stringIsValidDate } from '../utils/helpers'; -import { uploadImage } from './imageServices'; +import { + CreateUserData, + UpdateUserData, + UpdateUserInput, + UserWithoutId, + UserWithoutPasswordHash, +} from '../types'; +import { isObject, stringIsValidDate } from '../utils/helpers'; +import { uploadImage, URL } from './imageServices'; +import { updateSubcriber } from './notificationServices'; const USER_FIELDS_WITHOUT_PASSWORD_HASH = { id: true, @@ -19,8 +26,8 @@ const USER_FIELDS_WITHOUT_PASSWORD_HASH = { requests: { include: { user: true, - } - } + }, + }, }; // helpers @@ -46,16 +53,18 @@ const getAllUsers = async (): Promise> => { * @returns Promise resolving to username */ const parseAndValidateUsername = async (username: unknown): Promise => { -// Only contains alphanumeric characters, underscore and dot. -// Underscore and dot can't be at the end or start of a username (e.g _username / username_ / .username / username.). -// Underscore and dot can't be next to each other (e.g user_.name). -// Underscore or dot can't be used multiple times in a row (e.g user__name / user..name). + // Only contains alphanumeric characters, underscore and dot. + // Underscore and dot can't be at the end or start of a username (e.g _username / username_ / .username / username.). + // Underscore and dot can't be next to each other (e.g user_.name). + // Underscore or dot can't be used multiple times in a row (e.g user__name / user..name). // Number of characters must be between 4 and 15. const usernameRegex = /^(?=.{4,20}$)(?![.])(?!.*[.]{2})(?!.*[_]{3})[a-z0-9._]+(? => { }; /** - * - narrows type of password to string, generates password hash from the password - * - throws error if password is missing or invalid ie less than 4 characters - * @param password this should be a valid password - * @returns Promise resolving to password hash - */ + * - narrows type of password to string, generates password hash from the password + * - throws error if password is missing or invalid ie less than 4 characters + * @param password this should be a valid password + * @returns Promise resolving to password hash + */ const getPasswordHash = async (password: unknown): Promise => { const MINIMUM_PASSWORD_LENGTH = 4; if (typeof password !== 'string' || password.length < MINIMUM_PASSWORD_LENGTH) { @@ -120,13 +129,13 @@ const getPasswordHash = async (password: unknown): Promise => { * checks if username, password and email properties are present and of type string * @returns - type predicate (boolean) indicating whether obj is of type `CreateUserData` */ -const isCreateUserData = (obj: object): obj is CreateUserData => ( - 'username' in obj && 'password' in obj - && 'email' in obj - && typeof obj.username === 'string' - && typeof obj.password === 'string' - && typeof obj.email === 'string' -); +const isCreateUserData = (obj: object): obj is CreateUserData => + 'username' in obj && + 'password' in obj && + 'email' in obj && + typeof obj.username === 'string' && + typeof obj.password === 'string' && + typeof obj.email === 'string'; /** * - performs type narrowing for data from req.body to POST /users @@ -168,8 +177,9 @@ const parseCreateUserData = async (body: unknown): Promise => { * currently only username and password required * @returns Promise resolving to the user data without id */ -const generateUserDataWithoutId = async (createUserData: CreateUserData) -: Promise => { +const generateUserDataWithoutId = async ( + createUserData: CreateUserData, +): Promise => { const userData: UserWithoutId = { username: await parseAndValidateUsername(createUserData.username), password_hash: await getPasswordHash(createUserData.password), @@ -179,7 +189,7 @@ const generateUserDataWithoutId = async (createUserData: CreateUserData) dob: null, gender_id: null, bio: null, - image_url: createUserData.image_url || null + image_url: createUserData.image_url || null, }; return userData; @@ -191,7 +201,7 @@ const generateUserDataWithoutId = async (createUserData: CreateUserData) * @param userId * @returns Promise resolving to user data without password hash. */ -const getUserById = async (userId: number): Promise => { +const getUserById = async (userId: number): Promise => { const user: UserWithoutPasswordHash = await prismaClient.user.findUniqueOrThrow({ where: { id: userId, @@ -227,7 +237,12 @@ const isUpdateProfileData = (obj: object): obj is UpdateUserInput => { if ('dob' in obj && typeof obj.dob !== 'string') return false; if ('email' in obj && typeof obj.email !== 'string') return false; if ('bio' in obj && typeof obj.bio !== 'string') return false; - // if ('image_url' in obj && (typeof obj.image_url !== 'string' || obj.image_url instanceof File)) + if ( + 'image_url' in obj && + typeof obj.image_url !== 'string' && + typeof obj.image_url !== 'undefined' + ) + return false; return true; }; @@ -248,13 +263,13 @@ const updateUser = async (body: unknown, userId: number): Promise Date: Fri, 22 Mar 2024 16:38:53 +0200 Subject: [PATCH 08/13] Validate user token on get user(s) routes --- apps/backend/src/app.ts | 10 ++++ apps/backend/src/controllers/users.ts | 18 +++--- .../src/services/notificationServices.ts | 56 ++++++++++++------- apps/backend/src/services/userServices.ts | 4 +- apps/backend/src/utils/middleware.ts | 2 +- .../components/EditProfile/EditProfile.tsx | 19 ------- 6 files changed, 60 insertions(+), 49 deletions(-) diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index b259299c..e55f9515 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -1,4 +1,5 @@ import express from 'express'; +import * as formData from 'express-form-data'; import neighborhoodsRouter from './controllers/neighborhoods'; import notificationsRouter from './controllers/notifications'; import usersRouter from './controllers/users'; @@ -17,6 +18,15 @@ app.use(express.json()); app.use(middleware.requestLogger); app.use(middleware.tokenExtractor); +// parse data with connect-multiparty. +app.use(formData.parse()); +// delete from the request all empty files (size == 0) +app.use(formData.format()); +// change the file objects to fs.ReadStream +app.use(formData.stream()); +// union the body and the files +app.use(formData.union()); + // routes app.use('/api/neighborhoods', neighborhoodsRouter); app.use('/api/users', usersRouter); diff --git a/apps/backend/src/controllers/users.ts b/apps/backend/src/controllers/users.ts index 60004a3a..f700e70d 100644 --- a/apps/backend/src/controllers/users.ts +++ b/apps/backend/src/controllers/users.ts @@ -1,5 +1,4 @@ import express, { Request, Response } from 'express'; -import * as formData from 'express-form-data'; import catchError from '../utils/catchError'; import userServices from '../services/userServices'; import middleware from '../utils/middleware'; @@ -9,16 +8,17 @@ import { UserWithoutPasswordHash, RequestWithAuthentication } from '../types'; const usersRouter = express.Router(); // parse data with connect-multiparty. -usersRouter.use(formData.parse()); -// delete from the request all empty files (size == 0) -usersRouter.use(formData.format()); -// change the file objects to fs.ReadStream -usersRouter.use(formData.stream()); -// union the body and the files -usersRouter.use(formData.union()); +// usersRouter.use(formData.parse()); +// // delete from the request all empty files (size == 0) +// usersRouter.use(formData.format()); +// // change the file objects to fs.ReadStream +// usersRouter.use(formData.stream()); +// // union the body and the files +// usersRouter.use(formData.union()); usersRouter.get( '/', + middleware.userIdExtractorAndLoginValidator, catchError(async (_req: Request, res: Response) => { const users: Array = await userServices.getAllUsers(); @@ -28,6 +28,8 @@ usersRouter.get( usersRouter.get( '/:id', + middleware.userIdExtractorAndLoginValidator, + middleware.validateURLParams, catchError(async (req: Request, res: Response) => { const userId: number = Number(req.params.id); diff --git a/apps/backend/src/services/notificationServices.ts b/apps/backend/src/services/notificationServices.ts index 0d109709..39438941 100644 --- a/apps/backend/src/services/notificationServices.ts +++ b/apps/backend/src/services/notificationServices.ts @@ -135,28 +135,49 @@ export async function updateSubcriber({ firstName, lastName, email, - imageUrl, + avatar, }: { subscriberId: string; - firstName: string; - lastName: string; - email: string; - imageUrl?: string; + firstName?: string; + lastName?: string; + email?: string; + avatar?: string; }) { try { // If none of the values were updated ('' or undefined), return immediately - if ([firstName, lastName, email, imageUrl].every((val) => !val)) return; + if ([firstName, lastName, email, avatar].every((val) => !val)) return; - // eslint-disable-next-line prefer-rest-params - console.log(arguments); + // interface UpdateSubData { + // firstName?: string; + // lastName?: string; + // email?: string; + // imageUrl?: string; + // } - const res = await novu.subscribers.update(subscriberId, { - firstName, - lastName, - email, - avatar: imageUrl, - }); - console.log('novu res', res.data.data); + const subscriberInfo = { firstName, lastName, email, avatar }; + console.log({ subscriberInfo }); + + const options = { + method: 'PUT', + headers: { Authorization: `ApiKey ${NOVU_API_KEY}` }, + body: subscriberInfo as unknown as BodyInit, + }; + + const res = await fetch(`https://api.novu.co/v1/subscribers/${subscriberId}`, options) + .then((response) => response.json()) + .then((response) => response.data) + .catch((err) => { + console.error(err); + return { error: `Could not update subscriber ${subscriberId}.` }; + }); + + // const res = await novu.subscribers.update(subscriberId, { + // firstName, + // lastName, + // email, + // avatar: imageUrl, + // }); + console.log('novu res', res); } catch (error) { console.error(error); } @@ -259,17 +280,12 @@ export const triggers = { if (identicalNotification) return; - const subscriberInfo = (await getSubscriber(userId)).data; - console.log({subscriberInfo}); - await novu.trigger('join-neighborhood', { to: { subscriberId: adminId, }, actor: { subscriberId: userId, - // Maybe passing the id is enough to grab the avatar. Check - avatar: subscriberInfo.avatar }, payload: { neighborhoodId, diff --git a/apps/backend/src/services/userServices.ts b/apps/backend/src/services/userServices.ts index af46bdd3..0aeb407e 100644 --- a/apps/backend/src/services/userServices.ts +++ b/apps/backend/src/services/userServices.ts @@ -305,9 +305,11 @@ const updateUser = async (body: unknown, userId: number): Promise !Number.isNaN(val)); if (valid) return next(); - const error = new Error('unable to parse data'); + const error = new Error('Invalid URL.'); error.name = 'InvalidInputError'; throw error; }; diff --git a/apps/frontend/src/components/EditProfile/EditProfile.tsx b/apps/frontend/src/components/EditProfile/EditProfile.tsx index 1cae5511..be2f07ef 100644 --- a/apps/frontend/src/components/EditProfile/EditProfile.tsx +++ b/apps/frontend/src/components/EditProfile/EditProfile.tsx @@ -36,25 +36,6 @@ export default function EditProfile({ profile, closeForm }: Props) { Object.entries(formInput).forEach(([key, value]) => formData.append(key, value)); if (profPic) formData.append('image_url', profPic); - // if (profPic) { - // const URL = 'https://api.cloudinary.com/v1_1/dwlk6urra/image/upload'; - // const cloudFormData = new FormData(); - // cloudFormData.append('file', profPic); - // cloudFormData.append('upload_preset', 'prof_pic'); - // cloudFormData.append('overwrite', 'true'); - // cloudFormData.append('public_id', `${profile.username}:${profile.id}`); - // cloudFormData.append('api_key', process.env.REACT_APP_CLOUDINARY_API_KEY || ''); - - // const response = await fetch(URL, { - // method: 'post', - // body: cloudFormData, - // }).then(res => res.json()) - // .catch(console.error); - - // console.log(response); - // // secure_url - // } - const res = await userServices.updateProfile(formData, profile.id); console.log(res); From 0b6b9e88256791ad60b82f7cbc8bec533aa2b9ea Mon Sep 17 00:00:00 2001 From: nina-dk Date: Fri, 22 Mar 2024 16:43:29 +0200 Subject: [PATCH 09/13] Fix tests --- apps/backend/src/tests/requests_api.test.ts | 2 +- apps/backend/src/tests/testHelpers.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/tests/requests_api.test.ts b/apps/backend/src/tests/requests_api.test.ts index cd5a9bc9..4d43bc32 100644 --- a/apps/backend/src/tests/requests_api.test.ts +++ b/apps/backend/src/tests/requests_api.test.ts @@ -327,7 +327,7 @@ describe('Tests for deleting a request: DELETE /requests/:rId', () => { }); expect(response.status).toBe(400); - expect(response.body.error).toBe('unable to parse data'); + expect(response.body.error).toBe('Invalid URL.'); expect(request).not.toBe(null); }); diff --git a/apps/backend/src/tests/testHelpers.ts b/apps/backend/src/tests/testHelpers.ts index 51fbc723..7845ef74 100644 --- a/apps/backend/src/tests/testHelpers.ts +++ b/apps/backend/src/tests/testHelpers.ts @@ -41,6 +41,7 @@ const generateUserData = async (createUserData: CreateUserData): Promise Date: Fri, 22 Mar 2024 17:48:41 +0200 Subject: [PATCH 10/13] Send updated data to view; capture errors in state --- apps/backend/src/controllers/users.ts | 2 +- apps/backend/src/services/userServices.ts | 2 -- .../components/EditProfile/EditProfile.tsx | 29 +++++++++++------ .../src/pages/ProfilePage/ProfilePage.tsx | 32 +++++++++++-------- apps/frontend/src/types.ts | 14 +++++++- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/apps/backend/src/controllers/users.ts b/apps/backend/src/controllers/users.ts index f700e70d..99252f8d 100644 --- a/apps/backend/src/controllers/users.ts +++ b/apps/backend/src/controllers/users.ts @@ -64,7 +64,7 @@ usersRouter.put( middleware.userIdExtractorAndLoginValidator, catchError(async (req: RequestWithAuthentication, res: Response) => { const userId = Number(req.params.id); - if (!(userId === Number(req.loggedUserId))) { + if (userId !== Number(req.loggedUserId)) { return res.status(401).json('Logged user is not the owner of this profile'); } diff --git a/apps/backend/src/services/userServices.ts b/apps/backend/src/services/userServices.ts index 0aeb407e..065b698d 100644 --- a/apps/backend/src/services/userServices.ts +++ b/apps/backend/src/services/userServices.ts @@ -298,8 +298,6 @@ const updateUser = async (body: unknown, userId: number): Promise) => { event.preventDefault(); @@ -37,12 +37,23 @@ export default function EditProfile({ profile, closeForm }: Props) { if (profPic) formData.append('image_url', profPic); const res = await userServices.updateProfile(formData, profile.id); - console.log(res); + const responseData: ErrorObj | UpdatableUserFields = + 'error' in res + ? res + : { + first_name: res.first_name || '', + last_name: res.last_name || '', + email: res.email || '', + image_url: res.image_url || '', + dob: res.dob || null, + bio: res.bio || '', + }; + + submit(responseData as unknown as { [name: string]: string }, { + method: 'post', + action: `/users/${profile.id}`, + }); - // submit(formData, { - // method: 'put', - // action: `/users/${profile.id}`, - // }); closeForm(); }; diff --git a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx index 21639237..ff3b44b0 100644 --- a/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx +++ b/apps/frontend/src/pages/ProfilePage/ProfilePage.tsx @@ -1,5 +1,5 @@ -import { ActionFunctionArgs, LoaderFunctionArgs, useLoaderData } from 'react-router'; -import { useState } from 'react'; +import { ActionFunctionArgs, LoaderFunctionArgs, useActionData, useLoaderData } from 'react-router'; +import { useEffect, useState } from 'react'; import { UserWithRelatedData } from '@neighborhood/backend/src/types'; import { Container, Row, Col, Image } from 'react-bootstrap'; import CustomBtn from '../../components/CustomBtn/CustomBtn'; @@ -9,7 +9,8 @@ import usersServices from '../../services/users'; import EditProfile from '../../components/EditProfile/EditProfile'; import ProfileInfo from '../../components/ProfileInfo/ProfileInfo'; import CloudImg from '../../components/CloudImg/CouldImg'; -import { UpdateUserInput } from '../../types'; +import { ErrorObj, UpdatableUserFields } from '../../types'; +import AlertBox from '../../components/AlertBox/AlertBox'; export async function loader({ params }: LoaderFunctionArgs) { const userId = params.id; @@ -17,23 +18,27 @@ export async function loader({ params }: LoaderFunctionArgs) { return userData; } -export async function action({ params, request }: ActionFunctionArgs) { - const formData = await request.formData(); - const profileId = Number(params.id); +export async function action({ request }: ActionFunctionArgs) { + const res = await request.formData(); + const userData = Object.fromEntries(res) as unknown as UpdatableUserFields; - const profileData = Object.fromEntries(formData) as unknown as UpdateUserInput; - - const response = await usersServices.updateProfile(profileData, profileId); - return response; + return userData; } const profileImgPlaceholder = require('../../assets/icons/user_icon.png'); export default function ProfilePage() { const loggedUser = getStoredUser(); - const profileData = useLoaderData() as UserWithRelatedData; + let profileData = useLoaderData() as UserWithRelatedData; + const updatedData = useActionData() as UpdatableUserFields | ErrorObj | undefined; const [edit, setEdit] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (updatedData && 'error' in updatedData) setError(updatedData); + else if (updatedData) profileData = { ...profileData, ...updatedData }; + }, [updatedData]) function closeForm() { setEdit(false); @@ -57,11 +62,12 @@ export default function ProfilePage() { ) : ( ); - - console.log(profileData); return ( + + {error && } + {userImg} diff --git a/apps/frontend/src/types.ts b/apps/frontend/src/types.ts index f185184f..e2f3a9b7 100644 --- a/apps/frontend/src/types.ts +++ b/apps/frontend/src/types.ts @@ -50,7 +50,7 @@ export type UserInfo = Omit; /** * format of the data sent to `PUT /user/:id` to edit user */ -export type UpdateUserInput = { +export interface UpdateUserInput { first_name: string; last_name: string; bio: string; @@ -59,6 +59,18 @@ export type UpdateUserInput = { image_url?: File; }; +/** + * format of the data sent to `user/:id` action after user update + */ +export interface UpdatableUserFields { + first_name: string; + last_name: string; + bio: string; + email: string; + dob: Date | null; + image_url: string; +}; + export type SingleNeighborhoodFormIntent = | 'create-request' | 'join-neighborhood' From 7c851f8470b0ab9c9015321004a8d7fd2dbdee8c Mon Sep 17 00:00:00 2001 From: nina-dk Date: Fri, 22 Mar 2024 17:51:28 +0200 Subject: [PATCH 11/13] Fix height css bug --- apps/frontend/src/pages/ProfilePage/ProfilePage.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/pages/ProfilePage/ProfilePage.module.css b/apps/frontend/src/pages/ProfilePage/ProfilePage.module.css index bd3fc45d..69da0d9f 100644 --- a/apps/frontend/src/pages/ProfilePage/ProfilePage.module.css +++ b/apps/frontend/src/pages/ProfilePage/ProfilePage.module.css @@ -1,7 +1,7 @@ .container { - padding: 30px 30px 0 30px; + padding: 30px; margin: 0; - height: 100vh; + /* height: 100vh; */ } .column { From b2afa35915d5573685b8b4854a65fd25a8cf5ea3 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Fri, 22 Mar 2024 18:00:17 +0200 Subject: [PATCH 12/13] Comment out unused functions --- apps/backend/src/services/imageServices.ts | 58 +++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/services/imageServices.ts b/apps/backend/src/services/imageServices.ts index 75f4487e..b6ed0e2b 100644 --- a/apps/backend/src/services/imageServices.ts +++ b/apps/backend/src/services/imageServices.ts @@ -30,41 +30,41 @@ const uploadImage = async (imagePath: File | string, publicId: string): Promise< }; // Gets details of an uploaded image // -const getAssetInfo = async (publicId: number) => { - // Return colors in the response - const options = { - colors: true, - }; +// const getAssetInfo = async (publicId: number) => { +// // Return colors in the response +// const options = { +// colors: true, +// }; - try { - // Get details about the asset - const result = await cloudinary.api.resource(publicId, options); - console.log(result); - return result.colors; - } catch (error) { - console.error(error); - } -}; +// try { +// // Get details about the asset +// const result = await cloudinary.api.resource(publicId, options); +// console.log(result); +// return result.colors; +// } catch (error) { +// console.error(error); +// } +// }; // Creates an HTML image tag with a transformation that // results in a circular thumbnail crop of the image // focused on the faces, applying an outline of the // first color, and setting a background of the second color. -const createImageTag = (publicId: number, ...colors: string[]) => { - // Set the effect color and background color - const [effectColor, backgroundColor] = colors; +// const createImageTag = (publicId: number, ...colors: string[]) => { +// // Set the effect color and background color +// const [effectColor, backgroundColor] = colors; - // Create an image tag with transformations applied to the src URL - const imageTag = cloudinary.image(publicId, { - transformation: [ - { width: 250, height: 250, gravity: 'faces', crop: 'thumb' }, - { radius: 'max' }, - { effect: 'outline:10', color: effectColor }, - { background: backgroundColor }, - ], - }); +// // Create an image tag with transformations applied to the src URL +// const imageTag = cloudinary.image(publicId, { +// transformation: [ +// { width: 250, height: 250, gravity: 'faces', crop: 'thumb' }, +// { radius: 'max' }, +// { effect: 'outline:10', color: effectColor }, +// { background: backgroundColor }, +// ], +// }); - return imageTag; -}; +// return imageTag; +// }; -export { uploadImage, getAssetInfo, createImageTag }; +export { uploadImage }; From be0b1d1402d2f4e8dd081607721b640c1778f0e7 Mon Sep 17 00:00:00 2001 From: nina-dk Date: Fri, 22 Mar 2024 18:10:20 +0200 Subject: [PATCH 13/13] Return error if image upload fails --- apps/backend/src/services/imageServices.ts | 6 +++++- apps/backend/src/services/userServices.ts | 6 ++++++ apps/backend/src/types.ts | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/services/imageServices.ts b/apps/backend/src/services/imageServices.ts index b6ed0e2b..81a22800 100644 --- a/apps/backend/src/services/imageServices.ts +++ b/apps/backend/src/services/imageServices.ts @@ -1,3 +1,5 @@ +import { ErrorObj } from "../types"; + // Require the cloudinary library const cloudinary = require('cloudinary').v2; @@ -13,7 +15,7 @@ cloudinary.config({ console.log(cloudinary.config()); // Uploads an image file // -const uploadImage = async (imagePath: File | string, publicId: string): Promise => { +const uploadImage = async (imagePath: File | string, publicId: string): Promise => { // Allow overwriting the asset with new versions const options = { public_id: publicId, @@ -26,6 +28,8 @@ const uploadImage = async (imagePath: File | string, publicId: string): Promise< return result.public_id; } catch (error) { console.error(error); + + return { error: 'Sorry, we couldn\'t upload your image.'} } }; diff --git a/apps/backend/src/services/userServices.ts b/apps/backend/src/services/userServices.ts index 065b698d..ac65b004 100644 --- a/apps/backend/src/services/userServices.ts +++ b/apps/backend/src/services/userServices.ts @@ -266,6 +266,12 @@ const updateUser = async (body: unknown, userId: number): Promise