-
-
+
+ {renderImage()}
{actionButtons}
diff --git a/packages/shared/src/components/cards/entity/UserEntityCard.tsx b/packages/shared/src/components/cards/entity/UserEntityCard.tsx
index 31ce88ed4b..f77a95f3b8 100644
--- a/packages/shared/src/components/cards/entity/UserEntityCard.tsx
+++ b/packages/shared/src/components/cards/entity/UserEntityCard.tsx
@@ -32,6 +32,7 @@ import EntityDescription from './EntityDescription';
import useUserMenuProps from '../../../hooks/useUserMenuProps';
import useShowFollowAction from '../../../hooks/useShowFollowAction';
import type { MenuItemProps } from '../../dropdown/common';
+import { ProfilePictureWithDecoration } from '../../profile/ProfilePictureWithDecoration';
type Props = {
user?: UserShortProfile;
@@ -72,8 +73,7 @@ const UserEntityCard = ({ user, className }: Props) => {
},
[user, openModal],
);
- const { username, bio, name, image, isPlus, createdAt, id, permalink } =
- user || {};
+ const { username, bio, name, isPlus, createdAt, id, permalink } = user || {};
const options: MenuItemProps[] = [
{
icon:
,
@@ -120,13 +120,20 @@ const UserEntityCard = ({ user, className }: Props) => {
return (
+ }
actionButtons={
showActionBtns && (
<>
diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx
index 570bd67299..be4ecc0ec2 100644
--- a/packages/shared/src/components/modals/common.tsx
+++ b/packages/shared/src/components/modals/common.tsx
@@ -413,6 +413,13 @@ const CandidateSignInModal = dynamic(
),
);
+const DecorationSelectionModal = dynamic(
+ () =>
+ import(
+ /* webpackChunkName: "decorationSelectionModal" */ './decorations/DecorationSelectionModal'
+ ),
+);
+
const FeedbackModal = dynamic(
() => import(/* webpackChunkName: "feedbackModal" */ './FeedbackModal'),
);
@@ -484,6 +491,7 @@ export const modals = {
[LazyModal.SlackChannelConfirmation]: SlackChannelConfirmationModal,
[LazyModal.RecruiterSeats]: RecruiterSeatsModal,
[LazyModal.CandidateSignIn]: CandidateSignInModal,
+ [LazyModal.DecorationSelection]: DecorationSelectionModal,
[LazyModal.Feedback]: FeedbackModal,
};
diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts
index 47f91bebf8..98db9a18b2 100644
--- a/packages/shared/src/components/modals/common/types.ts
+++ b/packages/shared/src/components/modals/common/types.ts
@@ -91,6 +91,7 @@ export enum LazyModal {
SlackChannelConfirmation = 'slackChannelConfirmation',
RecruiterSeats = 'recruiterSeats',
CandidateSignIn = 'candidateSignIn',
+ DecorationSelection = 'decorationSelection',
Feedback = 'feedback',
}
diff --git a/packages/shared/src/components/modals/decorations/DecorationSelectionModal.tsx b/packages/shared/src/components/modals/decorations/DecorationSelectionModal.tsx
new file mode 100644
index 0000000000..6b5b50eaa2
--- /dev/null
+++ b/packages/shared/src/components/modals/decorations/DecorationSelectionModal.tsx
@@ -0,0 +1,208 @@
+import type { ReactElement } from 'react';
+import React, { useState } from 'react';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import classNames from 'classnames';
+import type { ModalProps } from '../common/Modal';
+import { Modal } from '../common/Modal';
+import {
+ Typography,
+ TypographyColor,
+ TypographyType,
+} from '../../typography/Typography';
+import { Button, ButtonVariant } from '../../buttons/Button';
+import { ProfilePictureWithDecoration } from '../../profile/ProfilePictureWithDecoration';
+import { ProfileImageSize } from '../../ProfilePicture';
+import { useAuthContext } from '../../../contexts/AuthContext';
+import {
+ DECORATIONS_BY_GROUP_QUERY,
+ SET_ACTIVE_DECORATION_MUTATION,
+} from '../../../graphql/decorations';
+import type { Decoration, DecorationGroup } from '../../../graphql/decorations';
+import { gqlClient } from '../../../graphql/common';
+import { LockIcon, VIcon } from '../../icons';
+import { useToastNotification } from '../../../hooks/useToastNotification';
+import { Loader } from '../../Loader';
+
+interface DecorationItemProps {
+ decoration: Decoration;
+ isSelected: boolean;
+ onSelect: (decoration: Decoration) => void;
+}
+
+const DecorationItem = ({
+ decoration,
+ isSelected,
+ onSelect,
+}: DecorationItemProps): ReactElement => {
+ const isLocked = !decoration.isUnlocked;
+
+ return (
+
+ );
+};
+
+export default function DecorationSelectionModal({
+ onRequestClose,
+ ...props
+}: ModalProps): ReactElement {
+ const { user, updateUser } = useAuthContext();
+ const { displayToast } = useToastNotification();
+ const [selectedDecoration, setSelectedDecoration] =
+ useState
(
+ user?.activeDecoration ? (user.activeDecoration as Decoration) : null,
+ );
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['decorationsByGroup'],
+ queryFn: async () => {
+ const result = await gqlClient.request<{
+ decorationsByGroup: DecorationGroup[];
+ }>(DECORATIONS_BY_GROUP_QUERY);
+ return result.decorationsByGroup;
+ },
+ });
+
+ const { mutate: setActiveDecoration, isPending } = useMutation({
+ mutationFn: async (decorationId: string | null) => {
+ const result = await gqlClient.request<{
+ setActiveDecoration: {
+ id: string;
+ activeDecoration: Decoration | null;
+ };
+ }>(SET_ACTIVE_DECORATION_MUTATION, { decorationId });
+ return result.setActiveDecoration;
+ },
+ onSuccess: (response) => {
+ updateUser({
+ ...user,
+ activeDecoration: response.activeDecoration,
+ });
+ displayToast('Avatar decoration updated');
+ onRequestClose?.(null);
+ },
+ onError: () => {
+ displayToast('Failed to update decoration');
+ },
+ });
+
+ const handleApply = () => {
+ setActiveDecoration(selectedDecoration?.id ?? null);
+ };
+
+ const handleClearDecoration = () => {
+ setSelectedDecoration(null);
+ };
+
+ const hasChanged =
+ (selectedDecoration?.id ?? null) !== (user?.activeDecoration?.id ?? null);
+ const isSelectedLocked = selectedDecoration && !selectedDecoration.isUnlocked;
+
+ return (
+
+
+
+
+
+ {selectedDecoration && (
+
+ )}
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ data?.map((group) => (
+
+
+ {group.label}
+
+
+ {group.decorations.map((decoration) => (
+
+ ))}
+
+
+ ))
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/shared/src/components/post/NewComment.tsx b/packages/shared/src/components/post/NewComment.tsx
index 5d662f3440..fde2f061aa 100644
--- a/packages/shared/src/components/post/NewComment.tsx
+++ b/packages/shared/src/components/post/NewComment.tsx
@@ -8,11 +8,8 @@ import React, {
} from 'react';
import classNames from 'classnames';
import { useRouter } from 'next/router';
-import {
- getProfilePictureClasses,
- ProfileImageSize,
- ProfilePicture,
-} from '../ProfilePicture';
+import { getProfilePictureClasses, ProfileImageSize } from '../ProfilePicture';
+import { ProfilePictureWithDecoration } from '../profile/ProfilePictureWithDecoration';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { Image } from '../image/Image';
import { fallbackImages } from '../../lib/config';
@@ -138,9 +135,10 @@ function NewCommentComponent(
onClick={() => onCommentClick(Origin.StartDiscussion)}
>
{user ? (
-
diff --git a/packages/shared/src/components/profile/ProfileHeader.tsx b/packages/shared/src/components/profile/ProfileHeader.tsx
index 8ac8673610..d528ebdbe6 100644
--- a/packages/shared/src/components/profile/ProfileHeader.tsx
+++ b/packages/shared/src/components/profile/ProfileHeader.tsx
@@ -7,13 +7,13 @@ import {
TypographyColor,
TypographyType,
} from '../typography/Typography';
-import { DevPlusIcon, EditIcon } from '../icons';
+import { DevPlusIcon, EditIcon, SparkleIcon } from '../icons';
import type { PublicProfile } from '../../lib/user';
import type { UserStatsProps } from './UserStats';
import { UserStats } from './UserStats';
import JoinedDate from './JoinedDate';
import { Separator } from '../cards/common/common';
-import { Button, ButtonVariant } from '../buttons/Button';
+import { Button, ButtonVariant, ButtonSize } from '../buttons/Button';
import { webappUrl } from '../../lib/constants';
import Link from '../utilities/Link';
import { useAuthContext } from '../../contexts/AuthContext';
@@ -22,6 +22,8 @@ import { VerifiedCompanyUserBadge } from '../VerifiedCompanyUserBadge';
import { locationToString } from '../../lib/utils';
import { IconSize } from '../Icon';
import { fallbackImages } from '../../lib/config';
+import { useLazyModal } from '../../hooks/useLazyModal';
+import { LazyModal } from '../modals/common/types';
import { ElementPlaceholder } from '../ElementPlaceholder';
@@ -57,20 +59,47 @@ const ProfileHeader = ({
isSameUser: propIsSameUser,
isPreviewMode,
}: ProfileHeaderProps) => {
- const { name, username, bio, image, cover, isPlus } = user;
+ const { name, username, bio, image, cover, isPlus, activeDecoration } = user;
const { user: loggedUser } = useAuthContext();
+ const { openModal } = useLazyModal();
const isSameUser = propIsSameUser ?? loggedUser?.id === user.id;
+
+ const handleOpenDecorationModal = () => {
+ openModal({ type: LazyModal.DecorationSelection });
+ };
+
return (
-
+
+
+ {activeDecoration && (
+
+ )}
+
+
+ {isSameUser && !isPreviewMode && (
+
}
+ onClick={handleOpenDecorationModal}
+ aria-label="Edit avatar decoration"
+ />
+ )}
+