diff --git a/src/components/ProfileInfo/ProfileBio/ProfileBio.tsx b/src/components/ProfileInfo/ProfileBio/ProfileBio.tsx index 7607ea51..28ff716d 100644 --- a/src/components/ProfileInfo/ProfileBio/ProfileBio.tsx +++ b/src/components/ProfileInfo/ProfileBio/ProfileBio.tsx @@ -1,11 +1,10 @@ import { Link } from 'react-router-dom'; import { ProfileImage } from '@/components/ProfileImage/ProfileImage'; -import { getFullName, getProfileImageUrl } from '@/utils/profileUtils'; import { ICON_EDIT_WHITE } from '@/utils/assets'; import { groupBannerUrl } from '@/utils/apiUtils'; -import type { Profile } from '@/types/Profile'; +import { type Profile, ProfileClass } from '@/types/Profile'; import { TypeLinkToBootstrapIcon, type Link as Link_API } from '@/types/Link'; import type { Group } from '@/types/Group'; import './ProfileBio.less'; @@ -26,6 +25,8 @@ export function ProfileBio(props: ProfileBioProps) { ? { backgroundImage: `url(${groupBannerUrl(props.organization)})` } : { backgroundColor: "var(--primary-color)" } + const profile = new ProfileClass(props.profile); + return (
@@ -42,11 +43,11 @@ export function ProfileBio(props: ProfileBioProps) {
- + { isOnOwnProfile - ?

{ getFullName(props.profile) }

- : { getFullName(props.profile) } + ?

{ profile.fullname }

+ : { profile.fullname } } { props.profile.bio === null || props.profile.bio.length === 0 diff --git a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx index 49f7c145..93dc7485 100644 --- a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx +++ b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx @@ -3,7 +3,6 @@ import { Link, redirect, useNavigate } from "react-router-dom"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; import { AuthContext } from "@/contexts/Auth"; import "./WelcomeUser.less" -import { getProfileImageUrl } from "@/utils/profileUtils"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu" export function WelcomeUser() { @@ -20,7 +19,7 @@ export function WelcomeUser() { return( !isLogged ? null :
- {setProfileClicked(!profileClicked)}}/> + {setProfileClicked(!profileClicked)}}/> { profileClicked ? diff --git a/src/contexts/Auth/AuthContext.tsx b/src/contexts/Auth/AuthContext.tsx index 3089fcbf..762db9fa 100644 --- a/src/contexts/Auth/AuthContext.tsx +++ b/src/contexts/Auth/AuthContext.tsx @@ -1,18 +1,18 @@ import { createContext } from "react"; import { User } from "@/types/User"; -import { Profile } from "@/types/Profile"; +import { type ProfileClass } from "@/types/Profile"; import type { Group } from "@/types/Group"; export type AuthContextType = { user : User | null; - profile: Profile | null; + profile: ProfileClass | null; organization: Group | null; - signin: (email : string, password: string, recaptchaToken: string | null) => Promise; - signinGoogle: () => Promise; + signin: (email : string, password: string, recaptchaToken: string | null) => Promise; + signinGoogle: () => Promise; signout: () => Promise; - updateLoggedUser: () => Promise; + updateLoggedUser: () => Promise; } export const AuthContext = createContext(null!); diff --git a/src/contexts/Auth/AuthProvider.tsx b/src/contexts/Auth/AuthProvider.tsx index f2442bf4..926f7b0c 100644 --- a/src/contexts/Auth/AuthProvider.tsx +++ b/src/contexts/Auth/AuthProvider.tsx @@ -1,12 +1,12 @@ import { ReactNode, useEffect, useState } from "react"; import { AuthContext } from "./AuthContext"; -import { Profile } from "@/types/Profile"; +import { ProfileClass } from "@/types/Profile"; import { UniversimeApi } from "@/services/UniversimeApi"; import { goTo } from "@/services/routes"; import type { Group } from "@/types/Group"; export const AuthProvider = ({ children }: { children: ReactNode }) => { - const [profile, setProfile] = useState(null); + const [profile, setProfile] = useState(null); const [organization, setOrganization] = useState(null); const [finishedLogin, setFinishedLogin] = useState(false); const user = profile?.user ?? null; @@ -90,5 +90,9 @@ async function getLoggedProfile() { if(!await UniversimeApi.Auth.validateToken()) { return null; } - return (await UniversimeApi.Profile.profile()).body?.profile ?? null; + + const responseProfile = (await UniversimeApi.Profile.profile()).body?.profile; + return responseProfile + ? new ProfileClass(responseProfile) + : null; } diff --git a/src/pages/Group/Group.tsx b/src/pages/Group/Group.tsx index f1fdff8d..9075aeb6 100644 --- a/src/pages/Group/Group.tsx +++ b/src/pages/Group/Group.tsx @@ -5,6 +5,7 @@ import { GroupContext, GroupIntro, GroupTabRenderer, GroupTabs, fetchGroupPageDa import { ProfileInfo } from "@/components/ProfileInfo/ProfileInfo"; import { AuthContext } from "@/contexts/Auth"; import "./Group.less"; +import { ProfileClass } from "@/types/Profile"; export function GroupPage() { const page = useLoaderData() as GroupPageLoaderResponse; @@ -72,11 +73,11 @@ export function GroupPage() { group: data.group!, loggedData: { isParticipant: data.loggedData?.isParticipant!, - profile: data.loggedData?.profile!, + profile: new ProfileClass(data.loggedData?.profile!), links: data.loggedData?.links ?? [], groups: data.loggedData?.groups ?? [], }, - participants: data.participants, + participants: data.participants.map(ProfileClass.new), subgroups: data.subGroups, currentContent: undefined, diff --git a/src/pages/Group/GroupContext.tsx b/src/pages/Group/GroupContext.tsx index 61fbe989..0d5730b8 100644 --- a/src/pages/Group/GroupContext.tsx +++ b/src/pages/Group/GroupContext.tsx @@ -1,13 +1,13 @@ import { createContext } from "react"; import { Group } from "@/types/Group"; -import { Profile } from "@/types/Profile"; +import { type ProfileClass } from "@/types/Profile"; import type { Content, Folder } from "@/types/Capacity"; import { Link } from "@/types/Link"; export type GroupContextType = null | { group: Group; subgroups: Group[]; - participants: Profile[]; + participants: ProfileClass[]; folders: Folder[]; currentContent: Folder | undefined; @@ -33,7 +33,7 @@ export type GroupContextType = null | { loggedData: { isParticipant: boolean; - profile: Profile; + profile: ProfileClass; links: Link[]; groups: Group[]; }; diff --git a/src/pages/Group/GroupTabs/GroupPeople/GroupPeople.tsx b/src/pages/Group/GroupTabs/GroupPeople/GroupPeople.tsx index c69aac12..b68ad1e9 100644 --- a/src/pages/Group/GroupTabs/GroupPeople/GroupPeople.tsx +++ b/src/pages/Group/GroupTabs/GroupPeople/GroupPeople.tsx @@ -3,9 +3,8 @@ import { Link } from "react-router-dom"; import { EMPTY_LIST_CLASS, GroupContext } from "@/pages/Group"; import { setStateAsValue } from "@/utils/tsxUtils"; -import { Profile } from "@/types/Profile"; +import { ProfileClass } from "@/types/Profile"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; -import { getFullName } from "@/utils/profileUtils"; import "./GroupPeople.less"; import { Filter } from "@/components/Filter/Filter"; @@ -33,7 +32,7 @@ export function GroupPeople() { ); } -function makePeopleList(people: Profile[], filter: string) { +function makePeopleList(people: ProfileClass[], filter: string) { if (people.length === 0) { return

Esse grupo não possui participantes.

} @@ -41,7 +40,7 @@ function makePeopleList(people: Profile[], filter: string) { const lowercaseFilter = filter.toLowerCase(); const filteredPeople = filter.length === 0 ? people - : people.filter(p => (getFullName(p)).toLowerCase().includes(lowercaseFilter)); + : people.filter(p => (p.fullname ?? "").toLowerCase().includes(lowercaseFilter)); if (filteredPeople.length === 0) { return

Nenhum participante encontrado com a pesquisa.

@@ -52,7 +51,7 @@ function makePeopleList(people: Profile[], filter: string) { .map(renderPerson); } -function renderPerson(person: Profile) { +function renderPerson(person: ProfileClass) { const linkToProfile = `/profile/${person.user.name}`; const imageUrl = person.image?.startsWith("/") @@ -66,7 +65,7 @@ function renderPerson(person: Profile) {
- {getFullName(person)} + {person.fullname}

{person.bio}

diff --git a/src/pages/ManageProfile/ManageProfile.tsx b/src/pages/ManageProfile/ManageProfile.tsx index b059d938..b645f765 100644 --- a/src/pages/ManageProfile/ManageProfile.tsx +++ b/src/pages/ManageProfile/ManageProfile.tsx @@ -4,7 +4,6 @@ import { Navigate, useLoaderData, useNavigate } from "react-router-dom"; import UniversimeApi from "@/services/UniversimeApi"; import { ManageProfileLinks, ManageProfileLoaderResponse, ManageProfilePassword, ManageProfileImage, getManageLinks, getProfileImage } from "@/pages/ManageProfile"; import { setStateAsValue } from "@/utils/tsxUtils"; -import { getProfileImageUrl } from "@/utils/profileUtils"; import { AuthContext } from "@/contexts/Auth"; import * as SwalUtils from "@/utils/sweetalertUtils"; @@ -38,7 +37,7 @@ export function ManageProfilePage() {
- +
Altere seu nome diff --git a/src/pages/ManageProfile/loader.ts b/src/pages/ManageProfile/loader.ts index 5a36ada4..25374cde 100644 --- a/src/pages/ManageProfile/loader.ts +++ b/src/pages/ManageProfile/loader.ts @@ -1,10 +1,9 @@ import UniversimeApi from "@/services/UniversimeApi"; -import { GENDER_OPTIONS } from "@/utils/profileUtils"; -import { Gender, Profile } from "@/types/Profile"; +import { type Gender, ProfileClass, GENDER_OPTIONS } from "@/types/Profile"; import { Link, TypeLink, TypeLinkToLabel } from "@/types/Link"; export type ManageProfileLoaderResponse = { - profile: Profile | null; + profile: ProfileClass | null; links: Link[]; genderOptions: { @@ -46,7 +45,7 @@ export async function ManageProfileLoader(): Promise ({ ...r, origin: new ProfileClass(r.origin), destiny: new ProfileClass(r.destiny) })); + return (

Últimas Recomendações

- { profileContext.profileListData.recommendationsReceived.length > 0 ? + { recommendationsReceived.length > 0 ?
{ - profileContext.profileListData.recommendationsReceived.map((recommendation, i) => { + recommendationsReceived.map((recommendation, i) => { if (i >= MAX_RECOMMENDATIONS_QUANTITY) return null; @@ -26,11 +28,11 @@ export function ProfileLastRecommendations() { return (
- +
- {getFullName(recommendation.origin)} + {recommendation.origin.fullname}

Recomendou pela competência:

{recommendation.competenceType.name}

diff --git a/src/pages/Profile/ProfilePage.tsx b/src/pages/Profile/ProfilePage.tsx index 94d2549d..6d71684a 100644 --- a/src/pages/Profile/ProfilePage.tsx +++ b/src/pages/Profile/ProfilePage.tsx @@ -12,6 +12,7 @@ import * as SwalUtils from "@/utils/sweetalertUtils"; import { AuthContext } from "@/contexts/Auth"; import { SelectionBar } from "./SelectionBar/SelectionBar"; +import { ProfileClass } from "@/types/Profile"; import './Profile.css'; export function ProfilePage() { @@ -27,7 +28,7 @@ export function ProfilePage() { accessingLoggedUser: loaderData.accessingLoggedUser, allCompetenceTypes: loaderData.allCompetenceTypes, editCompetence: null, - profile: loaderData.profile!, + profile: new ProfileClass(loaderData.profile!), profileListData: { achievements: loaderData.profileListData.achievements, competences: loaderData.profileListData.competences, diff --git a/src/pages/Profile/ProfileSettings/ProfileSettings.tsx b/src/pages/Profile/ProfileSettings/ProfileSettings.tsx index 76bbd3b5..94ff9f77 100644 --- a/src/pages/Profile/ProfileSettings/ProfileSettings.tsx +++ b/src/pages/Profile/ProfileSettings/ProfileSettings.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, MouseEvent, useContext, useMemo, useState } from 'react'; import { ProfileContext } from '@/pages/Profile'; import { Link, TypeLink, TypeLinkToBootstrapIcon, TypeLinkToLabel } from '@/types/Link'; -import { getFullName, separateFullName, GENDER_OPTIONS } from '@/utils/profileUtils'; +import { GENDER_OPTIONS, ProfileClass } from "@/types/Profile"; import { UniversimeApi } from '@/services/UniversimeApi'; import './ProfileSettings.css' @@ -31,7 +31,7 @@ export function ProfileSettings(props: ProfileSettingsProps) {

Nome

- +
@@ -183,7 +183,7 @@ export function ProfileSettings(props: ProfileSettingsProps) { ? (genderElement as HTMLSelectElement).value : ''; - const [name, lastname] = separateFullName(fullname); + const [name, lastname] = ProfileClass.separateFullname(fullname); UniversimeApi.Profile.edit({ profileId: profileContext.profile.id, diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx index 3991ce8f..d6c7e47c 100644 --- a/src/pages/Settings/RolesPage/RolesPage.tsx +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -7,11 +7,10 @@ import { AuthContext } from "@/contexts/Auth"; import { SettingsTitle, type RolesPageLoaderResponse, RolesPageFetch, SettingsDescription } from "@/pages/Settings"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; import { setStateAsValue } from "@/utils/tsxUtils"; -import { getFullName, getProfileImageUrl } from "@/utils/profileUtils"; import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; import * as SwalUtils from "@/utils/sweetalertUtils"; -import { type Profile } from "@/types/Profile"; +import { ProfileClass, type Profile } from "@/types/Profile"; import { UserAccessLevelLabel, type UserAccessLevel, compareAccessLevel } from "@/types/User"; import { type Optional } from "@/types/utils"; import "./RolesPage.less"; @@ -21,7 +20,7 @@ export function RolesPage() { const navigate = useNavigate(); const auth = useContext(AuthContext); - const [participants, participantsDispatch] = useReducer(participantsReducer, data.success ? data.participants : undefined); + const [participants, participantsDispatch] = useReducer(participantsReducer, data.success ? data.participants.map(ProfileClass.new) : undefined); const [filter, setFilter] = useState(""); if (!participants) { @@ -42,7 +41,7 @@ export function RolesPage() { return compareAccessLevel(a.user.accessLevel!, b.user.accessLevel!); } - return getFullName(a).localeCompare(getFullName(b)); + return (a.fullname ?? "").localeCompare(b.fullname ?? ""); }); const CHANGE_ROLE_OPTIONS: OptionInMenu[] = Object.entries(UserAccessLevelLabel).map(([role, label]) => ({ @@ -71,9 +70,9 @@ export function RolesPage() { const isOwnProfile = auth.profile!.id === profile.id; return
- +
-

{getFullName(profile)}

+

{profile.fullname}

{profile.bio}

@@ -105,18 +104,11 @@ export function RolesPage() { if (p.id !== action.profileId) return p; - const unchanging = data.participants + const originalRole = data.participants .find(p => p.id === action.profileId)! - .user.accessLevel === action.setRole; - - return { - ...p, - changed: unchanging ? undefined : true, - user: { - ...p.user, - accessLevel: action.setRole, - } - } + .user.accessLevel!; + + return new ProfileOnList(p, originalRole, action.setRole); }); } @@ -124,7 +116,7 @@ export function RolesPage() { const response = await RolesPageFetch(auth.organization!.id); participantsDispatch({ type: "SET_ALL", - setParticipants: response.success ? response.participants : undefined, + setParticipants: response.success ? response.participants.map(ProfileClass.new) : undefined, }); } @@ -154,7 +146,17 @@ export function RolesPage() { } } -type ProfileOnList = Profile & { changed?: true }; +class ProfileOnList extends ProfileClass { + public changed?: true; + + constructor(profile: Profile, originalRole: UserAccessLevel, newRole: UserAccessLevel) { + super(profile); + this.user.accessLevel = newRole; + this.changed = originalRole === newRole + ? true + : undefined; + } +} type ParticipantsReducerAction = { type: "SET_ROLE"; diff --git a/src/types/Profile.ts b/src/types/Profile.ts index 0acf5be5..21dddedf 100644 --- a/src/types/Profile.ts +++ b/src/types/Profile.ts @@ -1,6 +1,13 @@ import type { User } from "@/types/User"; +import { type Nullable } from "@/types/utils"; +import { IMG_DEFAULT_PROFILE } from "@/utils/assets"; export type Gender = "M" | "F" | "O"; +export const GENDER_OPTIONS: {[k in Gender]: string} = { + M: "Masculino", + F: "Feminino", + O: "Outro", +}; export type Profile = { id: string; @@ -12,3 +19,108 @@ export type Profile = { bio: string | null; creationDate: string; } + +export class ProfileClass implements Profile { + constructor(private profile: Profile) {} + + /** + * Builds the full name of the profile. + * + * @returns {(string | null)} The concatenation of the first and last names, with a space in between + * or `null` if both are null. + */ + get fullname(): Nullable { + const hasFirst = this.firstname && this.firstname.length > 0; + const hasLast = this.lastname && this.lastname.length > 0; + + if (!hasFirst && !hasLast) + return null; + + return ( this.firstname ?? "" ) + + ( hasFirst && hasLast ? " " : "" ) + + ( this.lastname ?? "" ); + } + + /** + * User readable gender name, instead of the API value. + */ + get genderName() { + if (this.gender) + return GENDER_OPTIONS[this.gender]; + + // todo: use a constant with this value + return "Não informado"; + } + + /** + * Image URL ready to be used on an ``. + */ + get imageUrl() { + if (this.image === null) + return IMG_DEFAULT_PROFILE; + + return import.meta.env.VITE_UNIVERSIME_API + "/profile/image/" + this.id; + } + + /** + * Created date as `Date` instead of string; + */ + get createdAt() { + return new Date(this.creationDate); + } + + /** + * Separates a full name into a first name and a last name. + * + * @param {string} fullname The full name to be separated. + * @returns {[string, string]} An 2 element array, where the first element is + * the first name and the second is the last name or `undefined`, if `fullname` + * doesn't have a last name. + */ + public static separateFullname(fullname: string): [string, string | undefined] { + fullname = fullname.trim(); + const spaceIndex = fullname.indexOf(" "); + + if (fullname.length === 0 || spaceIndex < 0) + return [fullname, undefined]; + + const firstname = fullname.slice(0, spaceIndex); + const lastname = fullname.slice(spaceIndex + 1); + + return [ + firstname, + lastname, + ]; + } + + /** + * The same as `new ProfileClass(profile)`, but can be used as a callback function. + */ + public static new(profile: Profile) { + return new ProfileClass(profile); + } + + /* Profile type getters and setters */ + get id() { return this.profile.id } + set id(id: string) { this.profile.id = id } + + get user() { return this.profile.user } + set user(user: User) { this.profile.user = user } + + get firstname() { return this.profile.firstname } + set firstname(firstname: Nullable) { this.profile.firstname = firstname } + + get lastname() { return this.profile.lastname } + set lastname(lastname: Nullable) { this.profile.lastname = lastname } + + get bio() { return this.profile.bio } + set bio(bio: Nullable) { this.profile.bio = bio } + + get gender() { return this.profile.gender } + set gender(gender: Nullable) { this.profile.gender = gender } + + get image() { return this.profile.image } + set image(image: Nullable) { this.profile.image = image } + + get creationDate() { return this.profile.creationDate } +} diff --git a/src/utils/assets.ts b/src/utils/assets.ts index 05c11815..15d27b19 100644 --- a/src/utils/assets.ts +++ b/src/utils/assets.ts @@ -1,5 +1,6 @@ -export const IMG_UNIVERSI_LOGO = "/assets/imgs/universi-me2.png"; -export const IMG_DCX_LOGO = "/assets/imgs/dcx-png 1.png"; +export const IMG_UNIVERSI_LOGO = "/assets/imgs/universi-me2.png"; +export const IMG_DCX_LOGO = "/assets/imgs/dcx-png 1.png"; +export const IMG_DEFAULT_PROFILE = "/assets/imgs/default_avatar.png"; export const ICON_CHEVRON_DOWN = "/assets/icons/chevron-down-1.svg"; export const ICON_CHEVRON_UP_BLACK = "/assets/icons/chevron-up-black.svg"; diff --git a/src/utils/profileUtils.ts b/src/utils/profileUtils.ts deleted file mode 100644 index 6c792698..00000000 --- a/src/utils/profileUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Profile, Gender } from "@/types/Profile" - -export const GENDER_OPTIONS = { - "M": "Masculino", - "F": "Feminino", - "O": "Outro", -} - -export function getFullName(profile: Profile): string { - const first = profile.firstname ?? ""; - const last = profile.lastname ?? ""; - - return `${first}${first != "" ? " " : ""}${last}` -} - -export function separateFullName(fullname: string): [string, string] { - const names = fullname.split(' '); - - if (names.length <= 0) - return ['', '']; - - return [ - names[0], - names.slice(1).join(' ') - ] -} - -export function getGenderName(gender: Gender | null | undefined): string { - return gender ? GENDER_OPTIONS[gender] : 'Não informado'; -} - -export function getProfileImageUrl(profile: Profile): string | null { - if (!profile.image) { - return "/assets/imgs/default_avatar.png"; - } - - return profile.image.startsWith("/") - ? `${import.meta.env.VITE_UNIVERSIME_API}${profile.image}` - : profile.image; -}