Profile page redesign #510
@@ -95,16 +95,17 @@ const IdeHeader = ({
|
||||
<div />
|
||||
)}
|
||||
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
|
||||
{ (!children)
|
||||
? <DefaultTopButtons
|
||||
{!children ? (
|
||||
<DefaultTopButtons
|
||||
project={project}
|
||||
projectTitle={projectTitle}
|
||||
_projectOwner={_projectOwner}
|
||||
handleRender={handleRender}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
: children
|
||||
}
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{/* <TopButton>Fork</TopButton> */}
|
||||
<div className="h-8 w-8">
|
||||
<NavPlusButton />
|
||||
@@ -117,88 +118,95 @@ const IdeHeader = ({
|
||||
|
||||
export default IdeHeader
|
||||
|
||||
|
||||
function DefaultTopButtons({ project, projectTitle, _projectOwner, handleRender, canEdit }) {
|
||||
return (<>
|
||||
{canEdit && !projectTitle && (
|
||||
<CaptureButton
|
||||
canEdit={canEdit}
|
||||
projectTitle={project?.title}
|
||||
userName={project?.user?.userName}
|
||||
shouldUpdateImage={!project?.mainImage}
|
||||
TheButton={({ onClick }) => (
|
||||
<TopButton
|
||||
onClick={onClick}
|
||||
name="Save Project Image"
|
||||
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<Svg name="camera" className="w-6 h-6 text-ch-blue-400" />
|
||||
</TopButton>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!projectTitle && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={handleRender}
|
||||
name={canEdit ? 'Save' : 'Preview'}
|
||||
>
|
||||
<Svg
|
||||
name={canEdit ? 'floppy-disk' : 'photograph'}
|
||||
className="w-6 h-6 text-ch-pink-500"
|
||||
function DefaultTopButtons({
|
||||
project,
|
||||
projectTitle,
|
||||
_projectOwner,
|
||||
handleRender,
|
||||
canEdit,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{canEdit && !projectTitle && (
|
||||
<CaptureButton
|
||||
canEdit={canEdit}
|
||||
projectTitle={project?.title}
|
||||
userName={project?.user?.userName}
|
||||
shouldUpdateImage={!project?.mainImage}
|
||||
TheButton={({ onClick }) => (
|
||||
<TopButton
|
||||
onClick={onClick}
|
||||
name="Save Project Image"
|
||||
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<Svg name="camera" className="w-6 h-6 text-ch-blue-400" />
|
||||
</TopButton>
|
||||
)}
|
||||
/>
|
||||
|
|
||||
</TopButton>
|
||||
)}
|
||||
{projectTitle && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={() =>
|
||||
navigate(routes.ide({ userName: _projectOwner, projectTitle }))
|
||||
}
|
||||
name="Editor"
|
||||
>
|
||||
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
|
||||
</TopButton>
|
||||
)}
|
||||
<Popover className="relative outline-none w-full h-full">
|
||||
{({ open }) => {
|
||||
return (
|
||||
<>
|
||||
<Popover.Button className="h-full w-full outline-none">
|
||||
<TopButton
|
||||
Tag="div"
|
||||
name="Share"
|
||||
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<Svg
|
||||
name="share"
|
||||
className="w-6 h-6 text-ch-purple-500 mt-1"
|
||||
/>
|
||||
</TopButton>
|
||||
</Popover.Button>
|
||||
{open && (
|
||||
<Popover.Panel className="absolute z-10 mt-4 right-0">
|
||||
<Tabs
|
||||
className="bg-ch-purple-gray-200 rounded-md shadow-md overflow-hidden text-gray-700"
|
||||
selectedTabClassName="bg-ch-gray-700 text-white"
|
||||
)}
|
||||
{!projectTitle && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={handleRender}
|
||||
name={canEdit ? 'Save' : 'Preview'}
|
||||
>
|
||||
<Svg
|
||||
name={canEdit ? 'floppy-disk' : 'photograph'}
|
||||
className="w-6 h-6 text-ch-pink-500"
|
||||
/>
|
||||
</TopButton>
|
||||
)}
|
||||
{projectTitle && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={() =>
|
||||
navigate(routes.ide({ userName: _projectOwner, projectTitle }))
|
||||
}
|
||||
name="Editor"
|
||||
>
|
||||
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
|
||||
</TopButton>
|
||||
)}
|
||||
<Popover className="relative outline-none w-full h-full">
|
||||
{({ open }) => {
|
||||
return (
|
||||
<>
|
||||
<Popover.Button className="h-full w-full outline-none">
|
||||
<TopButton
|
||||
Tag="div"
|
||||
name="Share"
|
||||
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<TabPanel>
|
||||
<FullScriptEncoding />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ExternalScript />
|
||||
</TabPanel>
|
||||
<Svg
|
||||
name="share"
|
||||
className="w-6 h-6 text-ch-purple-500 mt-1"
|
||||
/>
|
||||
</TopButton>
|
||||
</Popover.Button>
|
||||
{open && (
|
||||
<Popover.Panel className="absolute z-10 mt-4 right-0">
|
||||
<Tabs
|
||||
className="bg-ch-purple-gray-200 rounded-md shadow-md overflow-hidden text-gray-700"
|
||||
selectedTabClassName="bg-ch-gray-700 text-white"
|
||||
>
|
||||
<TabPanel>
|
||||
<FullScriptEncoding />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ExternalScript />
|
||||
</TabPanel>
|
||||
|
||||
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
|
||||
<Tab className="p-3 px-5">encoded script</Tab>
|
||||
<Tab className="p-3 px-5">external script</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Popover>
|
||||
</>)
|
||||
}
|
||||
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
|
||||
<Tab className="p-3 px-5">encoded script</Tab>
|
||||
<Tab className="p-3 px-5">external script</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,17 +11,24 @@ import Svg from 'src/components/Svg'
|
||||
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
|
||||
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
|
||||
|
||||
export function ImageFallback({ width = 100, imageId = 'CadHub/eia1kwru54g2kf02s2xx', className = '' }) {
|
||||
export function ImageFallback({
|
||||
|
Having a component that got that fallback user image without the image uploading functionality made me able to migrate the signed-in popover to HeadlessUI from MaterialUI, because I couldn't have a button within a button. I figured it's okay because the use case for the top nav (and Having a component that got that fallback user image without the image uploading functionality made me able to migrate the signed-in popover to HeadlessUI from MaterialUI, because I couldn't have a button within a button. I figured it's okay because the use case for the top nav (and `ProjectCard`) don't ever need the upload functionality.
That button within a button error in the console has been annoying me for ages, but obviously I hadn't done anything about, so thanks! That button within a button error in the console has been annoying me for ages, but obviously I hadn't done anything about, so thanks!
|
||||
width = 100,
|
||||
imageId = 'CadHub/eia1kwru54g2kf02s2xx',
|
||||
|
not relevant to this PR, but I do think there must be a better way of doing a fallback/empty-state than this image. not relevant to this PR, but I do think there must be a better way of doing a fallback/empty-state than this image.
I hear that. I'll add to my list, maybe Cloudinary has a documented best. I hear that. I'll add to my list, maybe Cloudinary has a documented best.
|
||||
className = '',
|
||||
}) {
|
||||
return (
|
||||
<div className="relative overflow-hidden w-full h-full">
|
||||
<CloudinaryImage
|
||||
className={"object-cover w-full h-full shadow overflow-hidden " + className }
|
||||
cloudName="irevdev"
|
||||
publicId={imageId}
|
||||
width={width}
|
||||
crop="scale"
|
||||
/>
|
||||
</div>)
|
||||
<div className="relative overflow-hidden w-full h-full">
|
||||
<CloudinaryImage
|
||||
className={
|
||||
'object-cover w-full h-full shadow overflow-hidden ' + className
|
||||
}
|
||||
cloudName="irevdev"
|
||||
publicId={imageId}
|
||||
width={width}
|
||||
crop="scale"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ImageUploader({
|
||||
@@ -82,10 +89,7 @@ export default function ImageUploader({
|
||||
{cloudinaryId && isEditable && (
|
||||
<button className="w-full py-1 absolute z-10 bg-ch-blue-650 bg-opacity-50 hover:bg-opacity-80 bottom-0 right-0 left-0 flex items-center justify-center text-ch-gray-300">
|
||||
<span className="font-fira-code text-sm leading-4">Update</span>
|
||||
<Svg
|
||||
name="pencil-solid"
|
||||
className=" h-4 w-4 ml-4 mb-2"
|
||||
/>
|
||||
<Svg name="pencil-solid" className=" h-4 w-4 ml-4 mb-2" />
|
||||
</button>
|
||||
)}
|
||||
{isEditable && <input {...getInputProps()} />}
|
||||
|
||||
@@ -12,43 +12,46 @@ interface KeyValueType {
|
||||
}
|
||||
|
||||
const KeyValue = ({
|
||||
keyName,
|
||||
children,
|
||||
hide = false,
|
||||
canEdit = false,
|
||||
onEdit,
|
||||
isEditable = false,
|
||||
bottom = false,
|
||||
className = "",
|
||||
} : KeyValueType) => {
|
||||
if (!children || hide) return null
|
||||
return (
|
||||
<div className={"flex flex-col " + className}>
|
||||
<div className={"text-ch-blue-400 font-fira-code flex items-center leading-4 text-sm whitespace-nowrap " + (bottom ? "order-2" : "")}>
|
||||
<span className={isEditable ? "text-ch-blue-300" : ""}>{keyName}</span>
|
||||
{canEdit &&
|
||||
(isEditable ? (
|
||||
<button
|
||||
className="font-fira-sans text-ch-gray-300 items-center ml-4 grid grid-flow-col-dense p-px px-2 gap-2 bg-ch-blue-500 bg-opacity-50 hover:bg-opacity-70 rounded-sm"
|
||||
id="rename-button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Svg
|
||||
name="check"
|
||||
className="w-6 h-6"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<span>Update</span>
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={onEdit}>
|
||||
<Svg name="pencil-solid" className="h-4 w-4 ml-4 mb-2" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={"text-ch-gray-300 " + (bottom ? "mb-1" : "mt-1")}>{children}</div>
|
||||
keyName,
|
||||
children,
|
||||
hide = false,
|
||||
canEdit = false,
|
||||
onEdit,
|
||||
isEditable = false,
|
||||
bottom = false,
|
||||
className = '',
|
||||
}: KeyValueType) => {
|
||||
if (!children || hide) return null
|
||||
return (
|
||||
<div className={'flex flex-col ' + className}>
|
||||
<div
|
||||
className={
|
||||
'text-ch-blue-400 font-fira-code flex items-center leading-4 text-sm whitespace-nowrap ' +
|
||||
(bottom ? 'order-2' : '')
|
||||
}
|
||||
>
|
||||
|
Allows the key to appear below (only visually) using CSS Allows the key to appear below (only visually) using CSS `order` property,
These comments aren't showing up as out of date which is worrying me @Irev-Dev , so weird. This component was overhauled, see this commit These comments aren't showing up as out of date which is worrying me @Irev-Dev , so weird. This component was overhauled, see [this commit](https://github.com/Irev-Dev/cadhub/commit/7b2be01430454fb599ec9448de427c19db55e7c5#diff-bbd6cdd539b9e9bc9272aa05cb7894082d87d6772f64ed849ba0385ba960e5a4R29-R35)
|
||||
<span className={isEditable ? 'text-ch-blue-300' : ''}>{keyName}</span>
|
||||
{canEdit &&
|
||||
(isEditable ? (
|
||||
<button
|
||||
className="font-fira-sans text-ch-gray-300 items-center ml-4 grid grid-flow-col-dense p-px px-2 gap-2 bg-ch-blue-500 bg-opacity-50 hover:bg-opacity-70 rounded-sm"
|
||||
|
Broke out into its own component to be used across both Broke out into its own component to be used across both `PartProfile` and `UserProfile`
I know you like this component but I kinda hate it, well hate is a sting word but the amount of input params is off putting for such a simple component. If we pulling it out to be reused I might be good to make a few changes.
How does that sound, would you mind? I know you like this component but I kinda hate it, well hate is a sting word but the amount of input params is off putting for such a simple component. If we pulling it out to be reused I might be good to make a few changes.
- hide should be removed entirely, why did I added it? like should we add a hid param to every component that's conditionally rendered 😩 . Cases where it was being used should just be replaced with `{!shouldHide && <KeyValue ... />}
- `isEditable` is a terrible name, The variable is for when the component is in a edit state, but i reads like whether the component is allowed to be edited or something. perhaps 'inEditMode`
- I liked your variable name `hasPermissionToEdit` could we rename `canEdit` to that as well? much clearer.
How does that sound, would you mind?
Yeah totally, love these ideas. I hear you it about the input params too. Yeah totally, love these ideas. I hear you it about the input params too.
I think I found something better with it! Take a look at it. I think I found something better with it! Take a look at it.
|
||||
id="rename-button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Svg name="check" className="w-6 h-6" strokeWidth={3} />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={onEdit}>
|
||||
<Svg name="pencil-solid" className="h-4 w-4 ml-4 mb-2" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={'text-ch-gray-300 ' + (bottom ? 'mb-1' : 'mt-1')}>
|
||||
|
I'm not requesting a change this is just an observation. I noticed in the headlessui docs they follow a pattern like this for condition classes At first I didn't like it because the result will often be I'm not requesting a change this is just an observation. I noticed in the [headlessui docs](https://headlessui.dev/react/menu#basic-example) they follow a pattern like this for condition classes
```
className={`${active && 'bg-blue-500'}`}
```
At first I didn't like it because the result will often be `className="false"` so even `className="false false"` if you have more of them, but I guess there's no harm in having classes that don't do anything in there
Oh interesting, yeah that's what I was avoiding here, but it would be shorter code their way. I guess the Oh interesting, yeah that's what I was avoiding here, but it would be shorter code their way. I guess the `false`s would have a negligible impact on things really.
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyValue
|
||||
export default KeyValue
|
||||
|
||||
@@ -52,7 +52,11 @@ const NavPlusButton: React.FC = () => {
|
||||
' px-4 py-1 my-4 bg-opacity-30 hover:bg-opacity-70 grid grid-flow-col-dense items-center gap-2'
|
||||
}
|
||||
>
|
||||
<div className={dotClasses + " justify-self-center w-5 h-5 rounded-full"}></div>
|
||||
<div
|
||||
className={
|
||||
dotClasses + ' justify-self-center w-5 h-5 rounded-full'
|
||||
}
|
||||
></div>
|
||||
<Link to={routes.draftProject({ cadPackage: ideType })}>
|
||||
<div>{name}</div>
|
||||
<div className="text-xs text-ch-gray-400 font-light">{sub}</div>
|
||||
|
||||
@@ -46,7 +46,8 @@ const ProfileSlashLogin = () => {
|
||||
<Popover className="relative outline-none h-8 w-8">
|
||||
<Popover.Button
|
||||
disabled={!isAuthenticated || !currentUser}
|
||||
className="h-full w-full outline-none border-ch-gray-400 border-2 rounded-full">
|
||||
className="h-full w-full outline-none border-ch-gray-400 border-2 rounded-full"
|
||||
>
|
||||
{!loading && (
|
||||
<ImageFallback
|
||||
width={80}
|
||||
@@ -55,8 +56,8 @@ const ProfileSlashLogin = () => {
|
||||
/>
|
||||
)}
|
||||
</Popover.Button>
|
||||
{ currentUser && (
|
||||
<Popover.Panel className="w-48 absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
{currentUser && (
|
||||
<Popover.Panel className="w-48 absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="text-lg hover:text-ch-pink-300">
|
||||
Hello {user?.name}
|
||||
@@ -65,14 +66,18 @@ const ProfileSlashLogin = () => {
|
||||
<hr className="my-2" />
|
||||
<Link
|
||||
className="my-2 mt-4 block hover:text-ch-pink-300"
|
||||
to={routes.user({ userName: user?.userName })}>
|
||||
to={routes.user({ userName: user?.userName })}
|
||||
>
|
||||
<div>View Your Profile</div>
|
||||
</Link>
|
||||
<a href="#" onClick={logOut}
|
||||
className="text-ch-gray-400 hover:text-ch-pink-300">
|
||||
<a
|
||||
href="#"
|
||||
onClick={logOut}
|
||||
className="text-ch-gray-400 hover:text-ch-pink-300"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</Popover.Panel>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</Popover>
|
||||
) : (
|
||||
|
||||
@@ -34,7 +34,7 @@ const ProjectCard = ({ title, mainImage, user, Reaction, cadPackage }) => (
|
||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-ch-gray-300 shadow">
|
||||
<ImageFallback
|
||||
imageId={user?.image} // http://res.cloudinary.com/irevdev/image/upload/c_scale,w_50/v1/CadHub/bc7smqwo9qqmrloyf9xr
|
||||
width={80} // http://res.cloudinary.com/irevdev/image/upload/c_scale,w_300/v1/CadHub/bc7smqwo9qqmrloyf9xr
|
||||
width={80} // http://res.cloudinary.com/irevdev/image/upload/c_scale,w_300/v1/CadHub/bc7smqwo9qqmrloyf9xr
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-lg text-ch-gray-300 font-fira-sans">
|
||||
|
||||
@@ -257,4 +257,3 @@ const ProjectProfile = ({
|
||||
}
|
||||
|
||||
export default ProjectProfile
|
||||
|
||||
|
||||
@@ -4,18 +4,24 @@ import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
|
||||
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import { fieldsConfig, fieldReducer, UserProfileType, FieldConfigType } from './userProfileConfig'
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
import {
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
fieldsConfig,
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
fieldReducer,
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
UserProfileType,
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
FieldConfigType,
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
} from './userProfileConfig'
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
|
||||
function buildFieldsConfig(fieldsConfig, user) {
|
||||
Object.entries(fieldsConfig).forEach(([key, field] : [string, FieldConfigType]) => {
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
field.currentValue = field.newValue = user[key]
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
field.name = key
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
})
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
Object.entries(fieldsConfig).forEach(
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
([key, field]: [string, FieldConfigType]) => {
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
field.currentValue = field.newValue = user[key]
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
field.name = key
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
}
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
)
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
|
||||
return fieldsConfig
|
||||
}
|
||||
|
||||
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
const UserProfile = ({
|
||||
user,
|
||||
isEditable,
|
||||
@@ -23,11 +29,13 @@ const UserProfile = ({
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
onSave,
|
||||
error,
|
||||
projects,
|
||||
} : UserProfileType) => {
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
}: UserProfileType) => {
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
const { currentUser } = useAuth()
|
||||
const hasEditPermission = currentUser?.sub === user.id
|
||||
useEffect(() => {
|
||||
isEditable && !hasEditPermission && navigate(routes.user({ userName: user.userName }))
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
isEditable &&
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
!hasEditPermission &&
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
navigate(routes.user({ userName: user.userName }))
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
}, [currentUser])
|
||||
|
||||
const initializedFields = buildFieldsConfig(fieldsConfig, user)
|
||||
@@ -49,8 +57,7 @@ const UserProfile = ({
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
projectOwnerImage={user?.image}
|
||||
projectOwnerId={user?.id}
|
||||
>
|
||||
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<span></span>
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<span></span>
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
</IdeHeader>
|
||||
</div>
|
||||
<div className="relative flex-grow h-full">
|
||||
@@ -63,7 +70,7 @@ const UserProfile = ({
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<fields.image.component
|
||||
field={fields.image}
|
||||
user={user}
|
||||
|
Check this out @Irev-Dev, I think I found something way better here. Using components instead of that big old config object. Check this out @Irev-Dev, I think I found something way better here. Using components instead of that big old config object.
|
||||
save={onSave}
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
save={onSave}
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
hasEditPermission={hasEditPermission}
|
||||
/>
|
||||
</div>
|
||||
@@ -95,14 +102,14 @@ const UserProfile = ({
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
/>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<fields.createdAt.component
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
field={fields.createdAt}
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
/>
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<fields.createdAt.component field={fields.createdAt} />
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
</div>
|
||||
</section>
|
||||
{/* Viewer */}
|
||||
<div className="py-10 px-8 w-full h-full relative bg-ch-gray-800 md:overflow-y-auto ch-scrollbar">
|
||||
<h3 className="text-2xl text-ch-gray-500 mb-4 md:hidden">Projects</h3>
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<h3 className="text-2xl text-ch-gray-500 mb-4 md:hidden">
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
Projects
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
</h3>
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
<ProjectsOfUser userName={user?.userName} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
Initializes values from Initializes values from `user` state into the config object. Is this a code smell?
|
||||
@@ -5,149 +5,187 @@ import Editor from 'rich-markdown-editor'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import { User } from 'types/graphql'
|
||||
|
||||
|
||||
export interface UserProfileType {
|
||||
user: User,
|
||||
isEditable: boolean,
|
||||
loading: boolean,
|
||||
error: boolean,
|
||||
onSave: Function,
|
||||
projects: {}[],
|
||||
user: User
|
||||
isEditable: boolean
|
||||
loading: boolean
|
||||
error: boolean
|
||||
onSave: Function
|
||||
projects: {}[]
|
||||
}
|
||||
|
||||
export interface FieldConfigType {
|
||||
name?: string, // introspection ugh
|
||||
editable: boolean,
|
||||
component?: ReactNode,
|
||||
needsRef?: boolean,
|
||||
isEditing?: boolean | undefined,
|
||||
onSave?: Function,
|
||||
currentValue?: any,
|
||||
newValue?: any,
|
||||
name?: string // introspection ugh
|
||||
editable: boolean
|
||||
component?: ReactNode
|
||||
needsRef?: boolean
|
||||
isEditing?: boolean | undefined
|
||||
|
Is there a Is there a `ProjectType` defined anywhere? I couldn't find one.
|
||||
onSave?: Function
|
||||
currentValue?: any
|
||||
newValue?: any
|
||||
}
|
||||
|
||||
const ProfileKeyValue = ({ field, dispatch, user, save, hasEditPermission, children, bottom = false }) => {
|
||||
const ProfileKeyValue = ({
|
||||
field,
|
||||
dispatch,
|
||||
user,
|
||||
save,
|
||||
hasEditPermission,
|
||||
children,
|
||||
bottom = false,
|
||||
}) => {
|
||||
return (
|
||||
<KeyValue
|
||||
keyName={field.name}
|
||||
hide={!user[field.name] && !hasEditPermission}
|
||||
canEdit={hasEditPermission}
|
||||
onEdit={() => {
|
||||
if (field.isEditing) {
|
||||
save(user.userName, { [field.name]: field.newValue })
|
||||
}
|
||||
dispatch({
|
||||
type: 'SET_CURRENT_VALUE',
|
||||
payload: { field: field.name, value: field.newValue },
|
||||
})
|
||||
dispatch({ type: 'TOGGLE_EDITING', payload: field.name })
|
||||
}}
|
||||
isEditable={hasEditPermission && field.isEditing}
|
||||
bottom={bottom}
|
||||
className="mb-4"
|
||||
>
|
||||
{children}
|
||||
</KeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
const bioField: FieldConfigType = {
|
||||
editable: true,
|
||||
needsRef: true,
|
||||
component: (props) => {
|
||||
const ref = useRef(null)
|
||||
|
||||
const { dispatch, field } = props
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
keyName={field.name}
|
||||
hide={!user[field.name] && !hasEditPermission}
|
||||
canEdit={hasEditPermission}
|
||||
onEdit={() => {
|
||||
if (field.isEditing) {
|
||||
save(user.userName, { [field.name]: field.newValue })
|
||||
<ProfileKeyValue {...props}>
|
||||
<div
|
||||
id="bio-wrap"
|
||||
name="bio"
|
||||
className={
|
||||
'markdown-overrides rounded-sm pb-2 mt-2' +
|
||||
(field.isEditable ? ' min-h-md' : '')
|
||||
}
|
||||
dispatch({ type: "SET_CURRENT_VALUE", payload: { field: field.name, value: field.newValue }})
|
||||
dispatch({ type: "TOGGLE_EDITING", payload: field.name })
|
||||
}}
|
||||
isEditable={hasEditPermission && field.isEditing}
|
||||
bottom={bottom}
|
||||
className="mb-4"
|
||||
>
|
||||
{ children }
|
||||
onClick={(e) =>
|
||||
e?.target?.id === 'bio-wrap' && ref?.current?.focusAtEnd()
|
||||
}
|
||||
>
|
||||
<Editor
|
||||
ref={ref}
|
||||
defaultValue={field?.currentValue || ''}
|
||||
readOnly={!field.isEditing}
|
||||
onChange={(bio) =>
|
||||
dispatch({
|
||||
type: 'SET_NEW_VALUE',
|
||||
payload: { field: field.bio, value: bio() },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const createdAtField: FieldConfigType = {
|
||||
editable: false,
|
||||
component: (props) => {
|
||||
const { field } = props
|
||||
|
||||
return (
|
||||
<KeyValue keyName="Member Since">
|
||||
<p className="text-ch-gray-300">
|
||||
{new Date(field.currentValue).toLocaleDateString()}
|
||||
</p>
|
||||
</KeyValue>
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const bioField : FieldConfigType = {
|
||||
editable: true,
|
||||
needsRef: true,
|
||||
component: (props) => {
|
||||
const ref = useRef(null)
|
||||
const imageField: FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { field, user, save, hasEditPermission } = props
|
||||
return (
|
||||
<ImageUploader
|
||||
className="rounded-3xl rounded-tr-none shadow-md border-2 border-ch-gray-300"
|
||||
onImageUpload={({ cloudinaryPublicId: image }) => {
|
||||
save(user.userName, {
|
||||
image,
|
||||
})
|
||||
}}
|
||||
aspectRatio={1}
|
||||
isEditable={hasEditPermission && !field.isEditing}
|
||||
imageUrl={user.image}
|
||||
width={300}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const { dispatch, field } = props
|
||||
const nameField: FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { user, dispatch, field } = props
|
||||
|
||||
return <ProfileKeyValue {...props}>
|
||||
<div
|
||||
id="bio-wrap"
|
||||
name="bio"
|
||||
className={
|
||||
'markdown-overrides rounded-sm pb-2 mt-2' +
|
||||
(field.isEditable ? ' min-h-md' : '')
|
||||
return (
|
||||
<ProfileKeyValue {...props} bottom={true}>
|
||||
{!field.isEditing ? (
|
||||
<h1 className="text-4xl">{user?.name}</h1>
|
||||
) : (
|
||||
<InputText
|
||||
className="text-xl"
|
||||
value={field.newValue}
|
||||
onChange={({ target: { value } }) =>
|
||||
dispatch({
|
||||
type: 'SET_NEW_VALUE',
|
||||
payload: { field: field.name, value },
|
||||
})
|
||||
}
|
||||
onClick={(e) =>
|
||||
e?.target?.id === 'bio-wrap' &&
|
||||
ref?.current?.focusAtEnd()
|
||||
isEditable={!field.isEditable}
|
||||
/>
|
||||
)}
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const userNameField: FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { dispatch, field } = props
|
||||
|
||||
return (
|
||||
<ProfileKeyValue {...props} bottom={true}>
|
||||
{!field.isEditing ? (
|
||||
<h2 className="text-ch-gray-400">
|
||||
@{field?.currentValue?.replace(/([^a-zA-Z\d_:])/g, '-')}
|
||||
</h2>
|
||||
) : (
|
||||
<InputText
|
||||
className="text-xl"
|
||||
value={field.newValue}
|
||||
onChange={({ target: { value } }) =>
|
||||
dispatch({
|
||||
type: 'SET_NEW_VALUE',
|
||||
payload: { field: field.name, value },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Editor
|
||||
ref={ref}
|
||||
defaultValue={field?.currentValue || ''}
|
||||
readOnly={!field.isEditing}
|
||||
onChange={(bio) => dispatch({ type: "SET_NEW_VALUE", payload: { field: field.bio, value: bio() }})}
|
||||
/>
|
||||
</div>
|
||||
</ProfileKeyValue>
|
||||
},
|
||||
}
|
||||
|
||||
const createdAtField : FieldConfigType = {
|
||||
editable: false,
|
||||
component: (props) => {
|
||||
const { field } = props
|
||||
|
||||
return <KeyValue keyName="Member Since">
|
||||
<p className="text-ch-gray-300">{ new Date(field.currentValue).toLocaleDateString() }</p>
|
||||
</KeyValue>
|
||||
},
|
||||
}
|
||||
|
||||
const imageField : FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { field, user, save, hasEditPermission } = props
|
||||
return (
|
||||
<ImageUploader
|
||||
className="rounded-3xl rounded-tr-none shadow-md border-2 border-ch-gray-300"
|
||||
onImageUpload={({ cloudinaryPublicId: image }) => {
|
||||
save(user.userName, {
|
||||
image,
|
||||
})
|
||||
}}
|
||||
aspectRatio={1}
|
||||
isEditable={hasEditPermission && !field.isEditing}
|
||||
imageUrl={user.image}
|
||||
width={300}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const nameField : FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { user, dispatch, field } = props
|
||||
|
||||
return <ProfileKeyValue {...props} bottom={true}>
|
||||
{ (!field.isEditing)
|
||||
? <h1 className="text-4xl">{ user?.name }</h1>
|
||||
: <InputText
|
||||
className="text-xl"
|
||||
value={field.newValue}
|
||||
onChange={({ target: { value } }) => dispatch({ type: "SET_NEW_VALUE", payload: { field: field.name, value }})}
|
||||
isEditable={!field.isEditable}
|
||||
/>
|
||||
}
|
||||
</ProfileKeyValue>
|
||||
},
|
||||
}
|
||||
|
||||
const userNameField : FieldConfigType = {
|
||||
editable: true,
|
||||
component: (props) => {
|
||||
const { dispatch, field } = props
|
||||
|
||||
return <ProfileKeyValue {...props} bottom={true}>
|
||||
{ (!field.isEditing)
|
||||
? <h2 className="text-ch-gray-400">@{ field?.currentValue?.replace(/([^a-zA-Z\d_:])/g, '-') }</h2>
|
||||
: <InputText
|
||||
className="text-xl"
|
||||
value={field.newValue}
|
||||
onChange={({ target: { value } }) => dispatch({ type: "SET_NEW_VALUE", payload: { field: field.name, value }})}
|
||||
isEditable={!field.isEditable}
|
||||
/>
|
||||
}
|
||||
</ProfileKeyValue>
|
||||
},
|
||||
isEditable={!field.isEditable}
|
||||
/>
|
||||
)}
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const fieldsConfig = {
|
||||
@@ -166,24 +204,27 @@ export const fieldsConfig = {
|
||||
|
||||
export function fieldReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "TOGGLE_EDITING":
|
||||
case 'TOGGLE_EDITING':
|
||||
return {
|
||||
...state,
|
||||
[action.payload]: {
|
||||
...state[action.payload],
|
||||
isEditing: (state[action.payload].editable && !state[action.payload].isEditing) ? true : false,
|
||||
}
|
||||
isEditing:
|
||||
state[action.payload].editable && !state[action.payload].isEditing
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
}
|
||||
case "SET_NEW_VALUE":
|
||||
case 'SET_NEW_VALUE':
|
||||
const newState = {
|
||||
...state,
|
||||
[action.payload.field]: {
|
||||
...state[action.payload.field],
|
||||
newValue: action.payload.value,
|
||||
}
|
||||
},
|
||||
}
|
||||
return newState
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,8 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<Popover className="relative outline-none w-full h-full">
|
||||
<Popover.Button
|
||||
disabled={!isAuthenticated || !currentUser}
|
||||
className="h-full w-full outline-none border-ch-gray-400 border-2 rounded-full">
|
||||
className="h-full w-full outline-none border-ch-gray-400 border-2 rounded-full"
|
||||
>
|
||||
{!loading && (
|
||||
<ImageFallback
|
||||
width={80}
|
||||
@@ -144,8 +145,8 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
/>
|
||||
)}
|
||||
</Popover.Button>
|
||||
{ currentUser && (
|
||||
<Popover.Panel className="w-48 absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
{currentUser && (
|
||||
<Popover.Panel className="w-48 absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="text-lg hover:text-ch-pink-300">
|
||||
Hello {user?.name}
|
||||
@@ -154,14 +155,18 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<hr className="my-2" />
|
||||
<Link
|
||||
className="my-2 mt-4 block hover:text-ch-pink-300"
|
||||
to={routes.user({ userName: user?.userName })}>
|
||||
to={routes.user({ userName: user?.userName })}
|
||||
>
|
||||
<div>View Your Profile</div>
|
||||
</Link>
|
||||
<a href="#" onClick={logOut}
|
||||
className="text-ch-gray-400 hover:text-ch-pink-300">
|
||||
<a
|
||||
href="#"
|
||||
onClick={logOut}
|
||||
className="text-ch-gray-400 hover:text-ch-pink-300"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</Popover.Panel>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</Popover>
|
||||
</li>
|
||||
|
||||
@@ -37,9 +37,7 @@ const AccountRecoveryPage = () => {
|
||||
className="grid items-center gap-2"
|
||||
style={{ gridTemplateColumns: 'auto 1fr' }}
|
||||
>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">
|
||||
email
|
||||
</span>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">email</span>
|
||||
<InputTextForm
|
||||
className="text-xl"
|
||||
name="email"
|
||||
|
||||
@@ -49,9 +49,7 @@ const UpdatePasswordPage = () => {
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">
|
||||
confirm
|
||||
</span>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">confirm</span>
|
||||
<InputTextForm
|
||||
className="text-xl"
|
||||
name="confirm"
|
||||
|
||||
Not sure this is the right move but I made it so a component could override the default buttons by bringing their own as children to it. It let me remove them by providing a dummy element without effecting any other pages.
Hmm I was trying to figure out what this even needed to be modified for the profile page but because this essentially the top nav bar.
I think this approach is good.
Perhaps later we'll look to remove
MainLayout.jsand replace it with the component everywhere. i.e. it can be the have the search bar for the projects explorer etc. If we do that than displaying children ONLY is probably better, i.e. it will needDefaultTopButtonsto be passed in, in this case.This is good though.
Yes I like that idea about
MainLayout, it's feeling about ready to be taken out after a little refactor of this header to cover more use cases.