From dd729fe23753e15147f70561a17825a8d96e333f Mon Sep 17 00:00:00 2001 From: Giuseppe Ernandez Date: Fri, 20 Mar 2026 20:01:41 +0100 Subject: [PATCH] fix: profile page refactor --- src/components/modal/index.tsx | 11 +- src/components/modal/styles.tsx | 27 +++- src/components/profile/profile-lists.tsx | 166 +++++++++++++++++++---- src/components/profile/profile-ui.tsx | 30 +++- src/components/profile/profile.tsx | 132 +++++++++++++----- src/components/profile/project-modal.tsx | 61 ++++++++- src/components/profile/styles.ts | 31 +++++ 7 files changed, 380 insertions(+), 78 deletions(-) diff --git a/src/components/modal/index.tsx b/src/components/modal/index.tsx index c6ccae4d..c6ddd5ea 100644 --- a/src/components/modal/index.tsx +++ b/src/components/modal/index.tsx @@ -24,8 +24,15 @@ function getModalStyle(width: number, height: number) { if (!width || !height) { return {}; } - const topOffset = window.innerHeight / 2 - height / 2; - const leftOffset = window.innerWidth / 2 - width / 2; + const viewportPadding = 2; + const topOffset = Math.max( + viewportPadding, + window.innerHeight / 2 - height / 2 + ); + const leftOffset = Math.max( + viewportPadding, + window.innerWidth / 2 - width / 2 + ); return { top: `${topOffset}px`, diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx index 3cbb0c94..3b73ace0 100644 --- a/src/components/modal/styles.tsx +++ b/src/components/modal/styles.tsx @@ -4,17 +4,32 @@ import { shadow, _scrollbars } from "@styles/_common"; export const content = (theme: Theme): SerializedStyles => css` position: absolute; outline: none; + max-width: calc(100vw - 6px); + max-height: calc(100vh - 6px); & > div { - max-height: 80vh; - overflow-y: scroll; + max-height: calc(100vh - 6px); + overflow-y: auto; + overflow-x: hidden; color: ${theme.textColor}; position: relative; - border: 2px solid ${theme.line}; - border-radius: 6px; + border: 0; + border-radius: 8px; background-color: ${theme.background}; ${shadow} - box-sizing: content-box; - padding: 16px 32px 24px; + box-sizing: border-box; + padding: 20px 28px 24px; + -webkit-overflow-scrolling: touch; ${_scrollbars(theme)} } + + @media (max-width: 760px) { + max-width: calc(100vw - 4px); + max-height: calc(100vh - 4px); + + & > div { + max-height: calc(100vh - 4px); + border-radius: 12px; + padding: 18px 16px 20px; + } + } `; diff --git a/src/components/profile/profile-lists.tsx b/src/components/profile/profile-lists.tsx index 4fd98ab1..64c6080c 100644 --- a/src/components/profile/profile-lists.tsx +++ b/src/components/profile/profile-lists.tsx @@ -1,7 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import { Link } from "react-router"; import { useDispatch, useSelector } from "@root/store"; import { List, ListItem, ListItemText } from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import LockIcon from "@mui/icons-material/Lock"; +import PublicIcon from "@mui/icons-material/Public"; import { selectFollowingLoading, selectFollowersLoading, @@ -15,7 +18,10 @@ import SettingsIcon from "@mui/icons-material/Settings"; import DeleteIcon from "@mui/icons-material/DeleteOutline"; import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; import Tooltip from "@mui/material/Tooltip"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; import { StyledListItemContainer, StyledListItemTopRowText, @@ -39,6 +45,40 @@ const ProjectListItem = ({ }) => { const dispatch = useDispatch(); const { isPublic, projectUid, name, description, tags } = project; + const isMobileLayout = useMediaQuery("(max-width: 760px)"); + const [menuAnchor, setMenuAnchor] = useState(null); + + const closeMenu = () => { + setMenuAnchor(null); + }; + + const openMenu = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setMenuAnchor(event.currentTarget); + }; + + const onEditProject = (event: React.MouseEvent) => { + dispatch(editProject(project)); + event.preventDefault(); + event.stopPropagation(); + closeMenu(); + }; + + const onDeleteProject = (event: React.MouseEvent) => { + dispatch(deleteProject(project)); + event.preventDefault(); + event.stopPropagation(); + closeMenu(); + }; + + const onTogglePublic = (event: React.MouseEvent) => { + dispatch(markProjectPublic(projectUid, !isPublic)); + event.preventDefault(); + event.stopPropagation(); + closeMenu(); + }; + return (
@@ -46,7 +86,48 @@ const ProjectListItem = ({ + {name} + + {isPublic ? ( + + ) : ( + + )} + {isPublic ? "Public" : "Private"} + + + } secondary={description} /> @@ -69,7 +150,9 @@ const ProjectListItem = ({ - {isProfileOwner && } + {isProfileOwner && !isMobileLayout && ( + + )} - {isProfileOwner && ( + {isProfileOwner && isMobileLayout && ( + <> + +
+
+ +
+
+
+ + + {isPublic ? ( + + ) : ( + + )} + + {isPublic + ? "Make project private" + : "Make project public"} + + + + + + Rename/Edit project + + + + + {`Delete ${name}`} + + + + )} + {isProfileOwner && !isMobileLayout && ( <>
{ - dispatch(editProject(project)); - event.preventDefault(); - event.stopPropagation(); - }} + onClick={onEditProject} >
@@ -98,14 +229,7 @@ const ProjectListItem = ({
-
{ - dispatch(deleteProject(project)); - event.preventDefault(); - event.stopPropagation(); - }} - > +
@@ -122,13 +246,7 @@ const ProjectListItem = ({
{ - dispatch( - markProjectPublic(projectUid, !isPublic) - ); - event.preventDefault(); - event.stopPropagation(); - }} + onClick={onTogglePublic} > {isPublic ? ( diff --git a/src/components/profile/profile-ui.tsx b/src/components/profile/profile-ui.tsx index c3b92411..7a7711d8 100644 --- a/src/components/profile/profile-ui.tsx +++ b/src/components/profile/profile-ui.tsx @@ -6,12 +6,14 @@ import { css } from "@emotion/react"; export const ProfileContainer = styled.div` ${isMobile() - ? `padding: 12.5vw;` + ? `padding: 16px; + box-sizing: border-box;` : `display: grid; grid-template-columns: 24px 250px 800px; grid-template-rows: 50px 175px 1fr 70px; grid-auto-rows: minmax(90px, auto);`} width: 100%; + overflow-x: hidden; `; export const IDContainer = styled(Card)` grid-row-start: 2; @@ -117,7 +119,7 @@ export const NameSectionWrapper = styled.div` display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr; - min-width: 680px; + min-width: ${isMobile() ? "0" : "680px"}; `; export const NameSection = styled.div` grid-row: 2; @@ -135,6 +137,8 @@ export const ContentSection = styled.div` border-radius: 4px; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.8); width: 100%; + max-width: 100%; + overflow-x: hidden; `; export const ContentTabsContainer = styled.div` background-color: rgba(0, 0, 0, 0.2); @@ -155,8 +159,12 @@ export const ListContainer = styled.div` padding-top: 10px; grid-row: 3; grid-column: 1; + width: 100%; + box-sizing: border-box; & > ul { padding: 0 !important; + width: 100%; + box-sizing: border-box; } .MuiListItem-button { padding: 8px 24px !important; @@ -263,3 +271,21 @@ export const fabButton = css` align-items: center; justify-content: space-between; `; + +export const mobileNavigationContainer = (theme: any) => css` + background-color: ${theme.headerBackground}; + position: fixed; + width: 100%; + bottom: 0; + left: 0; + z-index: 10; + border-top: 1px solid; +`; + +export const mobileNavigationButton = (theme: any) => css` + color: ${theme.headerTextColor}; +`; + +export const profileMobileBottomSpacer = css` + height: 72px; +`; diff --git a/src/components/profile/profile.tsx b/src/components/profile/profile.tsx index e6b90d18..cd03c3d6 100644 --- a/src/components/profile/profile.tsx +++ b/src/components/profile/profile.tsx @@ -12,8 +12,14 @@ import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; +import BottomNavigation from "@mui/material/BottomNavigation"; +import BottomNavigationAction from "@mui/material/BottomNavigationAction"; import CameraIcon from "@mui/icons-material/CameraAltOutlined"; import AddIcon from "@mui/icons-material/Add"; +import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; +import PersonAddAlt1OutlinedIcon from "@mui/icons-material/PersonAddAlt1Outlined"; +import PeopleOutlineIcon from "@mui/icons-material/PeopleOutline"; +import StarBorderOutlinedIcon from "@mui/icons-material/StarBorderOutlined"; import Box from "@mui/material/Box"; import SearchIcon from "@mui/icons-material/Search"; import TextField from "@mui/material/TextField"; @@ -65,7 +71,10 @@ import { contentActionsStyle, ListContainer, EditProfileButtonSection, - fabButton + fabButton, + mobileNavigationContainer, + mobileNavigationButton, + profileMobileBottomSpacer } from "./profile-ui"; const UserLink = ({ link }: { link: string | undefined }) => { @@ -367,43 +376,47 @@ export const Profile = () => { theme={theme} showSearch={selectedSection === 0} > - - { - switch (index) { - case 0: { - navigate(`/profile/${username}`); - break; + {!isMobile() && ( + + { + switch (index) { + case 0: { + navigate( + `/profile/${username}` + ); + break; + } + case 1: { + navigate( + `/profile/${username}/following` + ); + break; + } + case 2: { + navigate( + `/profile/${username}/followers` + ); + break; + } + case 3: { + navigate( + `/profile/${username}/stars` + ); + break; + } } - case 1: { - navigate( - `/profile/${username}/following` - ); - break; - } - case 2: { - navigate( - `/profile/${username}/followers` - ); - break; - } - case 3: { - navigate( - `/profile/${username}/stars` - ); - break; - } - } - }} - indicatorColor={"primary"} - > - - - - - - + }} + indicatorColor={"primary"} + > + + + + + + + )} { )} + {isMobile() &&
} + {isMobile() && ( + + navigate(`/profile/${username}`)} + css={mobileNavigationButton} + label="Projects" + icon={} + /> + + navigate(`/profile/${username}/following`) + } + css={mobileNavigationButton} + label="Following" + icon={ + + } + /> + + navigate(`/profile/${username}/followers`) + } + css={mobileNavigationButton} + label="Followers" + icon={} + /> + + navigate(`/profile/${username}/stars`) + } + css={mobileNavigationButton} + label="Stars" + icon={} + /> + + )} ); diff --git a/src/components/profile/project-modal.tsx b/src/components/profile/project-modal.tsx index 97a25cc9..404e49f2 100644 --- a/src/components/profile/project-modal.tsx +++ b/src/components/profile/project-modal.tsx @@ -20,11 +20,11 @@ import { SnackbarType } from "../snackbar/types"; import { closeModal } from "../modal/actions"; const avatarContainer = css` - margin-left: -16px; - margin-top: -16px; + margin-left: -10px; + margin-top: -10px; width: 62px; height: 62px; - padding: 28px; + padding: 14px; -webkit-box-pack: center; justify-content: center; cursor: pointer; @@ -36,9 +36,25 @@ const avatarContainer = css` const ModalContainer = styled.div` display: grid; - grid-auto-rows: minmax(90px, auto); - grid-template-columns: 400px; + grid-auto-rows: minmax(72px, auto); + grid-template-columns: min(400px, calc(100vw - 24px)); border-radius: 5px; + box-sizing: border-box; + padding: 6px 2px; + row-gap: 6px; + + h2 { + margin: 0; + line-height: 1.3; + } + + @media (max-width: 760px) { + grid-template-columns: calc(100vw - 24px); + grid-auto-rows: minmax(58px, auto); + row-gap: 8px; + padding: 8px 2px; + border-radius: 8px; + } `; interface IFieldRow { row: number; @@ -46,6 +62,11 @@ interface IFieldRow { const FieldRow = styled.div` grid-row: ${(properties) => properties.row}; grid-column: 1; + + @media (max-width: 760px) { + padding-left: 2px; + padding-right: 2px; + } `; const IconPickerContainer = styled.div` @@ -56,22 +77,47 @@ const IconPickerContainer = styled.div` grid-gap: 10px; padding: 4px; align-items: center; + + @media (max-width: 760px) { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto; + grid-gap: 12px; + justify-items: stretch; + } `; const StyledSketchPicker = styled(SliderPicker)` grid-column: 2; grid-row: 1; + + @media (max-width: 760px) { + grid-column: 1; + grid-row: 2; + width: 100% !important; + max-width: 100%; + } `; const RadioGroupContainer = styled.div` grid-column: 3; grid-row: 1; + + @media (max-width: 760px) { + grid-column: 1; + grid-row: 3; + } `; const PopoverContainer = styled.div` padding: 10px; width: 300px; height: 400px; + + @media (max-width: 760px) { + width: min(300px, calc(100vw - 40px)); + height: min(380px, calc(100vh - 120px)); + overflow-y: auto; + } `; interface IProjectModal { @@ -146,7 +192,7 @@ export const ProjectModal = (properties: IProjectModal) => { ); } }; - const textFieldStyle = { marginBottom: 12, marginRight: 5 }; + const textFieldStyle = { marginBottom: 12 }; const handleProfileDropDown = ( event: React.MouseEvent @@ -315,7 +361,8 @@ export const ProjectModal = (properties: IProjectModal) => { color="primary" disabled={shouldDisable} onClick={handleOnSubmit} - style={{ marginTop: 42 }} + style={{ marginTop: 18 }} + fullWidth > {properties.label} diff --git a/src/components/profile/styles.ts b/src/components/profile/styles.ts index 692e1f39..ef103268 100644 --- a/src/components/profile/styles.ts +++ b/src/components/profile/styles.ts @@ -214,6 +214,37 @@ export const publicIcon = (theme: Theme): SerializedStyles => css` } `; +export const mobileActionsContainer = css` + position: absolute; + top: calc(50% - 20px); + right: 12px; +`; + +export const mobileActionsButton = (theme: Theme): SerializedStyles => css` + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background-color: ${theme.highlightBackgroundAlt}; + color: ${theme.altTextColor}; + box-shadow: + 0 1px 3px black, + 0 1px 2px black; + cursor: pointer; + transition: background-color 0.2s ease; + + & > svg { + width: 24px; + height: 24px; + } + + &:hover { + background-color: ${theme.highlightBackground}; + } +`; + export const showAvatarPlayButton = css` .projectIcon { opacity: 0;