Merge branch 'main' into sidebar-tray
This commit is contained in:
@@ -22,6 +22,7 @@ module.exports = {
|
||||
800: '#1A1A1D',
|
||||
750: '#222222',
|
||||
760: '#232532',
|
||||
710: '#2B303C', // TODO: Use HSL so I stop adding grays to fix the warm/cool problem
|
||||
700: '#2A3038',
|
||||
600: '#3B3E4B',
|
||||
550: '#63636A',
|
||||
@@ -66,6 +67,9 @@ module.exports = {
|
||||
gridAutoColumns: {
|
||||
'preview-layout': 'minmax(30rem, 1fr) minmax(auto, 2fr)',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'profile-layout': 'minmax(32rem, 1fr) 2fr',
|
||||
},
|
||||
keyframes: {
|
||||
'bounce-sm': {
|
||||
'0%, 100%': {
|
||||
|
||||
@@ -43,8 +43,7 @@ export default function AssetWithGooey({
|
||||
<group ref={coffeeRef}>
|
||||
<mesh ref={mesh} scale={scaleArr} geometry={geo}>
|
||||
<meshPhysicalMaterial
|
||||
envMapIntensity={2}
|
||||
color="#F472B6"
|
||||
color="#FF6EBD"
|
||||
map={colorMap}
|
||||
clearcoat={0.5}
|
||||
clearcoatRoughness={0.01}
|
||||
@@ -59,6 +58,7 @@ export default function AssetWithGooey({
|
||||
</group>
|
||||
<ambientLight intensity={2} />
|
||||
<Gooey />
|
||||
<ambientLight intensity={1.8} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
@@ -73,22 +73,22 @@ function Gooey() {
|
||||
const dist = Math.random() * 3 + 2.5
|
||||
const x = randomSign(Math.random() * dist)
|
||||
const y = randomSign(Math.sqrt(dist * dist - x * x))
|
||||
const z = randomSign(Math.random() * 3)
|
||||
const z = randomSign(Math.random() * 2)
|
||||
const position: [number, number, number] = [x, z, y]
|
||||
const size = Math.random() * 0.8 + 0.1
|
||||
const distort = Math.random() * 0.8 + 0.1
|
||||
const speed = (Math.random() * 0.8) / size / size + 0.1
|
||||
const distort = (size > .1) ? Math.random() * .6 * size + 0.2 : 0
|
||||
const speed = (size > .1) ? (Math.random() * 0.8) / size / size + 0.1 : 0
|
||||
return { position, size, distort, speed }
|
||||
})
|
||||
const secondSet = Array.from({ length: 5 }).map((_, index) => {
|
||||
const dist = Math.random() * 3 + 1.5
|
||||
const x = randomSign(Math.random() * dist)
|
||||
const y = randomSign(Math.sqrt(dist * dist - x * x))
|
||||
const z = randomSign(Math.random() * 3)
|
||||
const z = randomSign(Math.random() * 2)
|
||||
const position: [number, number, number] = [x, z, y]
|
||||
const size = Math.random() * 0.2 + 0.05
|
||||
const distort = Math.random() * 0.8 + 0.1
|
||||
const speed = (Math.random() * 0.5) / size / size + 0.1
|
||||
const distort = (size > .1) ? Math.random() * .8 * size + 0.2 : 0
|
||||
const speed = (size > .1) ? (Math.random() * 0.5) / size / size + 0.1 : 0
|
||||
return { position, size, distort, speed }
|
||||
})
|
||||
return [...firstSet, ...secondSet]
|
||||
|
||||
@@ -11,6 +11,7 @@ import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
|
||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
|
||||
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
const TopButton = ({
|
||||
onClick,
|
||||
@@ -44,6 +45,7 @@ interface IdeHeaderProps {
|
||||
projectOwnerId?: string
|
||||
projectOwnerImage?: string
|
||||
projectId?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const IdeHeader = ({
|
||||
@@ -53,6 +55,7 @@ const IdeHeader = ({
|
||||
projectOwnerImage,
|
||||
projectId,
|
||||
projectOwnerId,
|
||||
children,
|
||||
}: IdeHeaderProps) => {
|
||||
const { currentUser } = useAuth()
|
||||
const { project } = useIdeContext()
|
||||
@@ -97,89 +100,19 @@ const IdeHeader = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
|
||||
{canEdit && !projectTitle && (
|
||||
<CaptureButton
|
||||
{!children ? (
|
||||
<DefaultTopButtons
|
||||
project={project}
|
||||
projectTitle={projectTitle}
|
||||
_projectOwner={_projectOwner}
|
||||
handleRender={handleRender}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
{!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"
|
||||
>
|
||||
<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>
|
||||
{/* <TopButton>Fork</TopButton> */}
|
||||
<div className="h-8 w-8 flex-shrink-0 rounded-full border-2 border-gray-200 flex items-center justify-center">
|
||||
<div className="h-8 w-8">
|
||||
<NavPlusButton />
|
||||
</div>
|
||||
<ProfileSlashLogin />
|
||||
@@ -189,3 +122,96 @@ 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"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,26 @@ 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 = '',
|
||||
}) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ImageUploader({
|
||||
onImageUpload = () => {},
|
||||
imageUrl,
|
||||
@@ -65,36 +85,24 @@ export default function ImageUploader({
|
||||
}
|
||||
style={{ paddingBottom: `${(1 / aspectRatio) * 100}%` }}
|
||||
>
|
||||
<div className="absolute w-full h-full" {...getRootProps()}>
|
||||
<div className="absolute w-full h-full inset-0" {...getRootProps()}>
|
||||
{cloudinaryId && isEditable && (
|
||||
<button className="absolute z-10 bg-indigo-900 opacity-75 bottom-0 right-0 flex items-center p-1 mb-6 mr-2 rounded-lg">
|
||||
<span className="text-gray-100 pr-2">Update</span>
|
||||
<Svg
|
||||
name="pencil"
|
||||
strokeWidth={2}
|
||||
className=" text-gray-100 h-6 w-6"
|
||||
/>
|
||||
<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" />
|
||||
</button>
|
||||
)}
|
||||
{isEditable && <input {...getInputProps()} />}
|
||||
{(cloudinaryId || !isEditable) && (
|
||||
<div className="relative overflow-hidden w-full h-full">
|
||||
<CloudinaryImage
|
||||
className="object-cover w-full h-full shadow overflow-hidden"
|
||||
cloudName="irevdev"
|
||||
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
|
||||
width={width}
|
||||
crop="scale"
|
||||
/>
|
||||
</div>
|
||||
<ImageFallback imageId={cloudinaryId} width={width} />
|
||||
)}
|
||||
{!cloudinaryId && <button className="absolute inset-0"></button>}
|
||||
{!cloudinaryId && isEditable && (
|
||||
<div className="text-indigo-500 flex items-center justify-center rounded-lg w-full h-full">
|
||||
<div className="px-6 text-center">
|
||||
Drop files here ... or{' '}
|
||||
<span className="group flex w-full items-center justify-center py-2">
|
||||
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">
|
||||
<div className="text-ch-blue-400 flex items-center justify-center rounded-lg w-full h-full">
|
||||
<div className="px-2 text-sm text-center">
|
||||
Drop files here or{' '}
|
||||
<span className="group flex w-full items-center justify-center pt-2">
|
||||
<span className="text-base bg-ch-blue-400 rounded-sm text-ch-gray-300 cursor-pointer px-3 py-2 leading-4 bg-ch-blue-700 bg-opacity-60 hover:bg-opacity-90">
|
||||
upload
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -23,7 +23,7 @@ const InputText = ({
|
||||
)}
|
||||
/>
|
||||
<input
|
||||
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
|
||||
className="text-ch-gray-300 rounded-none bg-ch-gray-600 border border-transparent focus:border-ch-gray-300 px-2 py-1 relative w-full"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
readOnly={!onChange}
|
||||
|
||||
@@ -10,7 +10,7 @@ const InputText = ({ type = 'text', className, name, validation }) => {
|
||||
<>
|
||||
<div className={getActiveClasses('relative mt-5', className)}>
|
||||
<FieldError
|
||||
className="absolute -my-5 text-sm text-red-500 font-ropa-sans"
|
||||
className="absolute -my-5 text-sm text-red-500"
|
||||
name={name}
|
||||
/>
|
||||
<TextField
|
||||
|
||||
63
app/web/src/components/KeyValue/KeyValue.tsx
Normal file
63
app/web/src/components/KeyValue/KeyValue.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
|
||||
interface EditToggleType {
|
||||
hasPermissionToEdit: boolean
|
||||
onEdit?: React.MouseEventHandler
|
||||
isEditing?: boolean
|
||||
}
|
||||
|
||||
const EditToggle = ({
|
||||
onEdit = () => { console.error('Field declared editable without edit action.') },
|
||||
isEditing = false,
|
||||
} : EditToggleType) => (
|
||||
(isEditing ? (
|
||||
<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>
|
||||
))
|
||||
)
|
||||
|
||||
interface KeyValueType {
|
||||
keyName: string
|
||||
children: React.ReactNode
|
||||
bottom?: boolean
|
||||
className?: string
|
||||
edit?: EditToggleType
|
||||
}
|
||||
|
||||
const KeyValue = ({
|
||||
keyName,
|
||||
children,
|
||||
bottom = false,
|
||||
className = '',
|
||||
edit = { hasPermissionToEdit: false },
|
||||
}: KeyValueType) => {
|
||||
if (!children) 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={edit ? 'text-ch-blue-300' : ''}>{keyName}</span>
|
||||
{edit && edit.hasPermissionToEdit && <EditToggle {...edit} /> }
|
||||
</div>
|
||||
<div className={'text-ch-gray-300 ' + (bottom ? 'mb-1' : 'mt-1')}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyValue
|
||||
@@ -40,8 +40,9 @@ const NavPlusButton: React.FC = () => {
|
||||
<Svg name="plus" className="text-ch-gray-300" />
|
||||
</Popover.Button>
|
||||
|
||||
<Popover.Panel className="absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
<Popover.Panel className="absolute w-48 z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
<p className="text-lg">New Project</p>
|
||||
<hr className="my-2" />
|
||||
<ul className="">
|
||||
{menuOptions.map(({ name, sub, ideType, bgClasses, dotClasses }) => (
|
||||
<li
|
||||
@@ -51,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 + ' 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>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useState } from 'react'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import ReactGA from 'react-ga'
|
||||
import Popover from '@material-ui/core/Popover'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ImageFallback } from 'src/components/ImageUploader'
|
||||
|
||||
import useUser from 'src/helpers/hooks/useUser'
|
||||
import LoginModal from 'src/components/LoginModal'
|
||||
@@ -42,60 +43,54 @@ const ProfileSlashLogin = () => {
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
{isAuthenticated ? (
|
||||
<div
|
||||
className="h-8 w-8 relative text-indigo-200"
|
||||
aria-describedby={popoverId}
|
||||
>
|
||||
<button
|
||||
className="absolute inset-0 w-full h-full"
|
||||
onClick={togglePopover}
|
||||
<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"
|
||||
>
|
||||
{!loading && <Gravatar image={user?.image} />}
|
||||
</button>
|
||||
</div>
|
||||
{!loading && (
|
||||
<ImageFallback
|
||||
width={80}
|
||||
className="rounded-full object-cover"
|
||||
imageId={user?.image}
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="text-lg hover:text-ch-pink-300">
|
||||
Hello {user?.name}
|
||||
</h3>
|
||||
</Link>
|
||||
<hr className="my-2" />
|
||||
<Link
|
||||
className="my-2 mt-4 block hover:text-ch-pink-300"
|
||||
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"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</Popover>
|
||||
) : (
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
className="text-indigo-200 font-semibold underline mr-2"
|
||||
className="text-sm text-ch-gray-300 mr-2 py-2 px-3 border-2 border-ch-gray-400 rounded-full hover:bg-ch-gray-600"
|
||||
onClick={recordedLogin}
|
||||
>
|
||||
Sign in/up
|
||||
Sign In/Up
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{isAuthenticated && currentUser && (
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={closePopover}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 w-48">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="text-indigo-800" style={{ fontWeight: '500' }}>
|
||||
Hello {user?.name}
|
||||
</h3>
|
||||
</Link>
|
||||
<hr />
|
||||
<br />
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<div className="text-indigo-800">Your Profile</div>
|
||||
</Link>
|
||||
<a href="#" className="text-indigo-800" onClick={logOut}>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
<LoginModal
|
||||
open={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
|
||||
@@ -4,10 +4,11 @@ import CadPackage from 'src/components/CadPackage/CadPackage'
|
||||
|
||||
import { countEmotes } from 'src/helpers/emote'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import { ImageFallback } from '../ImageUploader/ImageUploader'
|
||||
|
||||
const ProjectCard = ({ title, mainImage, user, Reaction, cadPackage }) => (
|
||||
<li
|
||||
className="rounded p-1.5 bg-ch-gray-760 shadow-ch"
|
||||
className="rounded p-1.5 bg-ch-gray-760 hover:bg-ch-gray-710 shadow-ch"
|
||||
key={`${user?.userName}--${title}`}
|
||||
>
|
||||
<Link
|
||||
@@ -31,11 +32,9 @@ const ProjectCard = ({ title, mainImage, user, Reaction, cadPackage }) => (
|
||||
</div>
|
||||
<div className="flex items-center mt-1">
|
||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-ch-gray-300 shadow">
|
||||
<ImageUploader
|
||||
className=""
|
||||
aspectRatio={1}
|
||||
imageUrl={user?.image}
|
||||
width={50}
|
||||
<ImageFallback
|
||||
imageId={user?.image}
|
||||
width={80}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-lg text-ch-gray-300 font-fira-sans">
|
||||
|
||||
@@ -17,51 +17,7 @@ import { useIdeInit } from 'src/components/EncodedUrl/helpers'
|
||||
import ProfileViewer from '../ProfileViewer/ProfileViewer'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import OpenscadStaticImageMessage from 'src/components/OpenscadStaticImageMessage/OpenscadStaticImageMessage'
|
||||
|
||||
const KeyValue = ({
|
||||
keyName,
|
||||
children,
|
||||
hide = false,
|
||||
canEdit = false,
|
||||
onEdit,
|
||||
isEditable = false,
|
||||
}: {
|
||||
keyName: string
|
||||
children: React.ReactNode
|
||||
hide?: boolean
|
||||
canEdit?: boolean
|
||||
onEdit?: () => void
|
||||
isEditable?: boolean
|
||||
}) => {
|
||||
if (!children || hide) return null
|
||||
return (
|
||||
<div>
|
||||
<div className="text-ch-blue-400 font-fira-code flex text-sm whitespace-nowrap">
|
||||
{keyName}
|
||||
{canEdit &&
|
||||
(isEditable ? (
|
||||
<button
|
||||
className="font-fira-sans items-center ml-4 grid grid-flow-col-dense p-px px-2 gap-2 bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 rounded-sm border border-ch-purple-400"
|
||||
id="rename-button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Svg
|
||||
name="check"
|
||||
className="w-6 h-6 text-ch-purple-500"
|
||||
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">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import KeyValue from 'src/components/KeyValue/KeyValue'
|
||||
|
||||
const ProjectProfile = ({
|
||||
userProject,
|
||||
@@ -71,7 +27,7 @@ const ProjectProfile = ({
|
||||
onComment,
|
||||
}) => {
|
||||
const [comment, setComment] = useState('')
|
||||
const [isEditable, setIsEditable] = useState(false)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const onCommentClear = () => {
|
||||
onComment(comment)
|
||||
setComment('')
|
||||
@@ -80,14 +36,14 @@ const ProjectProfile = ({
|
||||
const [isReactionsModalOpen, setIsReactionsModalOpen] = useState(false)
|
||||
const { currentUser } = useAuth()
|
||||
const editorRef = useRef(null)
|
||||
const canEdit =
|
||||
const hasPermissionToEdit =
|
||||
currentUser?.sub === userProject.id || currentUser?.roles.includes('admin')
|
||||
const project = userProject?.Project
|
||||
const emotes = countEmotes(project?.Reaction)
|
||||
const userEmotes = project?.userReactions.map(({ emote }) => emote)
|
||||
useEffect(() => {
|
||||
isEditable &&
|
||||
!canEdit &&
|
||||
isEditing &&
|
||||
!hasPermissionToEdit &&
|
||||
navigate(
|
||||
routes.project({
|
||||
userName: userProject.userName,
|
||||
@@ -99,7 +55,7 @@ const ProjectProfile = ({
|
||||
const [newDescription, setNewDescription] = useState(project?.description)
|
||||
const onDescriptionChange = (description) => setNewDescription(description())
|
||||
const onEditSaveClick = () => {
|
||||
if (isEditable) {
|
||||
if (isEditing) {
|
||||
onSave(project?.id, { description: newDescription })
|
||||
return
|
||||
}
|
||||
@@ -140,7 +96,7 @@ const ProjectProfile = ({
|
||||
</div>
|
||||
|
||||
{/* Side panel */}
|
||||
<div className="bg-ch-gray-760 font-fira-sans px-20 pt-12 overflow-y-auto">
|
||||
<div className="bg-ch-gray-760 font-fira-sans px-20 pt-12 overflow-y-auto ch-scrollbar">
|
||||
<div className="grid grid-flow-row-dense gap-6">
|
||||
<h3 className="text-5xl capitalize text-ch-gray-300">
|
||||
{project?.title.replace(/-/g, ' ')}
|
||||
@@ -152,26 +108,27 @@ const ProjectProfile = ({
|
||||
className="px-3 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<KeyValue
|
||||
{ (project?.description || hasPermissionToEdit) && <KeyValue
|
||||
keyName="Description"
|
||||
hide={!project?.description && !canEdit}
|
||||
canEdit={canEdit}
|
||||
onEdit={() => {
|
||||
if (!isEditable) {
|
||||
setIsEditable(true)
|
||||
} else {
|
||||
onEditSaveClick()
|
||||
setIsEditable(false)
|
||||
}
|
||||
edit={{
|
||||
hasPermissionToEdit,
|
||||
isEditing,
|
||||
onEdit: () => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true)
|
||||
} else {
|
||||
onEditSaveClick()
|
||||
setIsEditing(false)
|
||||
}
|
||||
},
|
||||
}}
|
||||
isEditable={isEditable}
|
||||
>
|
||||
<div
|
||||
id="description-wrap"
|
||||
name="description"
|
||||
className={
|
||||
'markdown-overrides rounded-sm pb-2 mt-2' +
|
||||
(isEditable ? ' min-h-md' : '')
|
||||
(isEditing ? ' min-h-md' : '')
|
||||
}
|
||||
onClick={(e) =>
|
||||
e?.target?.id === 'description-wrap' &&
|
||||
@@ -181,11 +138,11 @@ const ProjectProfile = ({
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
defaultValue={project?.description || ''}
|
||||
readOnly={!isEditable}
|
||||
readOnly={!isEditing}
|
||||
onChange={onDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</KeyValue>
|
||||
</KeyValue> }
|
||||
<div className="grid grid-flow-col-dense gap-6">
|
||||
<KeyValue keyName="Created on">
|
||||
{new Date(project?.createdAt).toDateString()}
|
||||
@@ -200,10 +157,11 @@ const ProjectProfile = ({
|
||||
userEmotes={userEmotes}
|
||||
onEmote={onReaction}
|
||||
onShowProjectReactions={() => setIsReactionsModalOpen(true)}
|
||||
className=""
|
||||
/>
|
||||
</KeyValue>
|
||||
<KeyValue keyName="Comments" hide={!currentUser}>
|
||||
{!isEditable && (
|
||||
{ currentUser && <KeyValue keyName="Comments">
|
||||
{!isEditing && (
|
||||
<>
|
||||
{currentUser && (
|
||||
<>
|
||||
@@ -259,8 +217,8 @@ const ProjectProfile = ({
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</KeyValue>
|
||||
{canEdit && (
|
||||
</KeyValue> }
|
||||
{hasPermissionToEdit && (
|
||||
<>
|
||||
<h4 className="mt-10 text-red-600">Danger Zone</h4>
|
||||
<Button
|
||||
|
||||
@@ -36,7 +36,7 @@ const ProjectsList = ({
|
||||
return (
|
||||
<section className="max-w-7xl mx-auto">
|
||||
<ul
|
||||
className="grid gap-x-8 gap-y-8 items-center mx-4 relative"
|
||||
className="grid gap-x-8 gap-y-8 items-center relative"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}
|
||||
>
|
||||
{filteredProjects.map(
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import Editor from 'rich-markdown-editor'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import Button from 'src/components/Button'
|
||||
import ProfileTextInput from 'src/components/ProfileTextInput'
|
||||
import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
|
||||
|
||||
const UserProfile = ({
|
||||
user,
|
||||
isEditable,
|
||||
loading,
|
||||
onSave,
|
||||
error,
|
||||
projects,
|
||||
}) => {
|
||||
const { currentUser } = useAuth()
|
||||
const canEdit = currentUser?.sub === user.id
|
||||
const isImageEditable = !isEditable && canEdit // image is editable when not in profile edit mode in order to separate them as it's too hard too to upload an image to cloudinary temporarily until the use saves (and maybe have to clean up) for the time being
|
||||
useEffect(() => {
|
||||
isEditable && !canEdit && navigate(routes.user({ userName: user.userName }))
|
||||
}, [currentUser])
|
||||
const [input, setInput] = useState({
|
||||
userName: user.userName,
|
||||
name: user.name,
|
||||
bio: user.bio,
|
||||
image: user.image,
|
||||
})
|
||||
const { userName, name } = input
|
||||
const editableTextFields = { userName, name }
|
||||
return (
|
||||
<>
|
||||
<section className="max-w-2xl mx-auto mt-20 ">
|
||||
<div className="flex">
|
||||
{!isEditable && (
|
||||
<div className="w-40 flex-shrink-0">
|
||||
<ImageUploader
|
||||
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
|
||||
onImageUpload={({ cloudinaryPublicId: image }) => {
|
||||
onSave(user.userName, {
|
||||
...input,
|
||||
image,
|
||||
})
|
||||
}}
|
||||
aspectRatio={1}
|
||||
isEditable={isImageEditable}
|
||||
imageUrl={user.image}
|
||||
width={300}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-6 flex flex-col justify-between">
|
||||
<ProfileTextInput
|
||||
fields={editableTextFields}
|
||||
onChange={({ userName, name }) =>
|
||||
setInput({
|
||||
...input,
|
||||
name,
|
||||
userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'),
|
||||
})
|
||||
}
|
||||
isEditable={isEditable}
|
||||
/>
|
||||
{isEditable ? (
|
||||
<Button
|
||||
className="bg-indigo-200"
|
||||
iconName="plus"
|
||||
onClick={() => onSave(user.userName, input)}
|
||||
>
|
||||
Save Profile
|
||||
</Button> // TODO replace pencil with a save icon
|
||||
) : canEdit ? (
|
||||
<Button
|
||||
className="bg-indigo-200"
|
||||
iconName="pencil"
|
||||
onClick={() =>
|
||||
navigate(routes.editUser({ userName: user.userName }))
|
||||
}
|
||||
>
|
||||
Edit Profile
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<h3 className="text-3xl text-gray-500 font-ropa-sans">Bio:</h3>
|
||||
<div
|
||||
name="description"
|
||||
className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-6 min-h-md"
|
||||
>
|
||||
<Editor
|
||||
defaultValue={user.bio || ''}
|
||||
readOnly={!isEditable}
|
||||
onChange={(bioFn) =>
|
||||
setInput({
|
||||
...input,
|
||||
bio: bioFn(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<h3 className="text-3xl text-gray-500 font-ropa-sans">Projects:</h3>
|
||||
<ProjectsOfUser userName={user?.userName} />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfile
|
||||
131
app/web/src/components/UserProfile/UserProfile.tsx
Normal file
131
app/web/src/components/UserProfile/UserProfile.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useReducer } from 'react'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
import ProjectsOfUser from 'src/components/ProjectsOfUserCell'
|
||||
import IdeHeader from 'src/components/IdeHeader/IdeHeader'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import {
|
||||
fieldComponents,
|
||||
fieldReducer,
|
||||
UserProfileType,
|
||||
FieldType,
|
||||
} from './userProfileConfig'
|
||||
|
||||
// This function initializes the state management object for each of the fields
|
||||
function buildFieldsConfig(fieldsConfig, user, hasPermissionToEdit) {
|
||||
return Object.fromEntries(Object.keys(fieldsConfig).map(
|
||||
(key: string): [string, FieldType] => ([key, {
|
||||
name: key,
|
||||
currentValue: user[key],
|
||||
newValue: user[key],
|
||||
isEditing: false,
|
||||
hasPermissionToEdit,
|
||||
}])
|
||||
))
|
||||
}
|
||||
|
||||
const UserProfile = ({
|
||||
user,
|
||||
isEditing,
|
||||
loading,
|
||||
onSave,
|
||||
error,
|
||||
}: UserProfileType) => {
|
||||
const { currentUser } = useAuth()
|
||||
const hasPermissionToEdit = currentUser?.sub === user.id
|
||||
useEffect(() => {
|
||||
isEditing &&
|
||||
!hasPermissionToEdit &&
|
||||
navigate(routes.user({ userName: user.userName }))
|
||||
}, [currentUser])
|
||||
|
||||
const initializedFields = buildFieldsConfig(fieldComponents, user, hasPermissionToEdit)
|
||||
const [fields, fieldDispatch] = useReducer(fieldReducer, initializedFields)
|
||||
const {
|
||||
name: NameField,
|
||||
userName: UserNameField,
|
||||
image: ImageField,
|
||||
bio: BioField,
|
||||
createdAt: MemberSinceField,
|
||||
} = fieldComponents
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="md:h-screen flex flex-col text-lg font-fira-sans">
|
||||
<div className="flex">
|
||||
<Link
|
||||
to={routes.home()}
|
||||
className="w-16 h-16 flex items-center justify-center bg-ch-gray-900"
|
||||
>
|
||||
<Svg className="w-12" name="favicon" />
|
||||
</Link>
|
||||
<IdeHeader
|
||||
handleRender={() => {}}
|
||||
projectOwner={user?.userName}
|
||||
projectOwnerImage={user?.image}
|
||||
projectOwnerId={user?.id}
|
||||
>
|
||||
<span></span>
|
||||
</IdeHeader>
|
||||
</div>
|
||||
<div className="relative flex-grow h-full">
|
||||
<div className="grid md:grid-cols-profile-layout grid-flow-row-dense absolute inset-0">
|
||||
{/* Side panel */}
|
||||
<section className="bg-ch-gray-760 font-fira-sans p-12 md:overflow-y-auto ch-scrollbar">
|
||||
<div className="flex gap-6">
|
||||
{!isEditing && (
|
||||
<div className="w-28 flex-shrink-0">
|
||||
<ImageField
|
||||
field={fields.image}
|
||||
dispatch={fieldDispatch}
|
||||
user={user}
|
||||
save={onSave}
|
||||
hasPermissionToEdit={hasPermissionToEdit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<NameField
|
||||
field={fields.name}
|
||||
dispatch={fieldDispatch}
|
||||
user={user}
|
||||
save={onSave}
|
||||
hasPermissionToEdit={hasPermissionToEdit}
|
||||
/>
|
||||
<UserNameField
|
||||
field={fields.userName}
|
||||
dispatch={fieldDispatch}
|
||||
user={user}
|
||||
save={onSave}
|
||||
hasPermissionToEdit={hasPermissionToEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<BioField
|
||||
field={fields.bio}
|
||||
dispatch={fieldDispatch}
|
||||
user={user}
|
||||
save={onSave}
|
||||
hasPermissionToEdit={hasPermissionToEdit}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<MemberSinceField field={fields.createdAt} />
|
||||
</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>
|
||||
<ProjectsOfUser userName={user?.userName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfile
|
||||
216
app/web/src/components/UserProfile/userProfileConfig.tsx
Normal file
216
app/web/src/components/UserProfile/userProfileConfig.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { ReactNode, useRef } from 'react'
|
||||
import KeyValue from 'src/components/KeyValue/KeyValue'
|
||||
import InputText from '../InputText/InputText'
|
||||
import Editor from 'rich-markdown-editor'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import { User } from 'types/graphql'
|
||||
|
||||
export const fieldComponents = {
|
||||
name: NameField,
|
||||
userName: UserNameField,
|
||||
image: ImageField,
|
||||
bio: BioField,
|
||||
createdAt: MemberSinceField,
|
||||
}
|
||||
|
||||
export interface UserProfileType {
|
||||
user: User
|
||||
isEditing: boolean
|
||||
loading: boolean
|
||||
error: boolean
|
||||
onSave: Function
|
||||
projects: {}[]
|
||||
}
|
||||
|
||||
export interface FieldType {
|
||||
name: string
|
||||
currentValue: any
|
||||
newValue: any
|
||||
isEditing: boolean
|
||||
hasPermissionToEdit: boolean
|
||||
}
|
||||
|
||||
export interface FieldComponentPropsType {
|
||||
field: FieldType
|
||||
dispatch?: React.Dispatch<Object>
|
||||
user?: User
|
||||
save?: Function
|
||||
hasPermissionToEdit?: boolean
|
||||
}
|
||||
|
||||
interface ProfileKeyValueType extends FieldComponentPropsType {
|
||||
children: ReactNode
|
||||
bottom: boolean
|
||||
}
|
||||
|
||||
const ProfileKeyValue = ({
|
||||
field,
|
||||
dispatch,
|
||||
user,
|
||||
save,
|
||||
hasPermissionToEdit,
|
||||
children,
|
||||
bottom = false,
|
||||
} : ProfileKeyValueType) => {
|
||||
return (
|
||||
(user[field.name] && hasPermissionToEdit) && <KeyValue
|
||||
keyName={field.name}
|
||||
edit={{
|
||||
hasPermissionToEdit,
|
||||
isEditing: field.isEditing,
|
||||
onEdit: () => {
|
||||
if (field.isEditing && field.currentValue !== field.newValue) {
|
||||
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 })
|
||||
},
|
||||
}}
|
||||
bottom={bottom}
|
||||
className="mb-4"
|
||||
>
|
||||
{children}
|
||||
</KeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
function BioField(props) {
|
||||
const ref = useRef(null)
|
||||
const { field, dispatch } = props
|
||||
|
||||
return (
|
||||
<ProfileKeyValue {...props}>
|
||||
<div
|
||||
id="bio-wrap"
|
||||
name="bio"
|
||||
className={
|
||||
'markdown-overrides rounded-sm pb-2 mt-2' +
|
||||
(field.isEditing ? ' min-h-md' : '')
|
||||
}
|
||||
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: 'bio', value: bio() },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
function MemberSinceField(props : FieldComponentPropsType) {
|
||||
return (
|
||||
<KeyValue keyName="Member Since">
|
||||
<p className="text-ch-gray-300">
|
||||
{new Date(props.field.currentValue).toLocaleDateString()}
|
||||
</p>
|
||||
</KeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
function ImageField(props : FieldComponentPropsType) {
|
||||
const { field, user, save, hasPermissionToEdit } = 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={hasPermissionToEdit}
|
||||
imageUrl={user.image}
|
||||
width={300}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NameField(props : FieldComponentPropsType) {
|
||||
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: 'name', value },
|
||||
})
|
||||
}
|
||||
isEditable={field.isEditing}
|
||||
/>
|
||||
)}
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
function UserNameField(props : FieldComponentPropsType) {
|
||||
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: 'userName', value },
|
||||
})
|
||||
}
|
||||
isEditable={field.isEditing}
|
||||
/>
|
||||
)}
|
||||
</ProfileKeyValue>
|
||||
)
|
||||
}
|
||||
|
||||
export function fieldReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_EDITING':
|
||||
return {
|
||||
...state,
|
||||
[action.payload]: {
|
||||
...state[action.payload],
|
||||
isEditing:
|
||||
state[action.payload].hasPermissionToEdit && !state[action.payload].isEditing
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
}
|
||||
case 'SET_NEW_VALUE':
|
||||
const newState = {
|
||||
...state,
|
||||
[action.payload.field]: {
|
||||
...state[action.payload.field],
|
||||
newValue: action.payload.value,
|
||||
},
|
||||
}
|
||||
return newState
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,18 @@
|
||||
font-family: 'Fira Sans', ui-sans-serif, system-ui, -apple-system, system-ui, "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.ch-scrollbar::-webkit-scrollbar {
|
||||
@apply w-3;
|
||||
}
|
||||
|
||||
.ch-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-ch-gray-700;
|
||||
}
|
||||
|
||||
.ch-scrollbar::-webkit-scrollbar-thumb {
|
||||
@apply bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-60;
|
||||
}
|
||||
|
||||
/* https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/ */
|
||||
.visually-hidden {
|
||||
clip: rect(0 0 0 0);
|
||||
@@ -39,6 +51,7 @@
|
||||
}
|
||||
.tabToggle.disabled {
|
||||
@apply text-ch-gray-550 cursor-not-allowed;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Toaster, toast } from '@redwoodjs/web/toast'
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import Popover from '@material-ui/core/Popover'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { getActiveClasses } from 'get-active-classes'
|
||||
import Footer from 'src/components/Footer'
|
||||
import { useLocation } from '@redwoodjs/router'
|
||||
@@ -13,7 +13,7 @@ import ReactGA from 'react-ga'
|
||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||
|
||||
import Svg from 'src/components/Svg'
|
||||
import ImageUploader from 'src/components/ImageUploader'
|
||||
import { ImageFallback } from 'src/components/ImageUploader'
|
||||
import useUser from 'src/helpers/hooks/useUser'
|
||||
|
||||
let previousSubmission = ''
|
||||
@@ -91,7 +91,7 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
}
|
||||
}, [hash, client])
|
||||
return (
|
||||
<div>
|
||||
<div className="min-h-screen flex flex-col ch-scrollbar">
|
||||
<header id="cadhub-main-header">
|
||||
<nav className="flex justify-between h-16 sm:px-4 bg-ch-gray-900">
|
||||
<ul className="flex items-center">
|
||||
@@ -131,20 +131,44 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<NavPlusButton />
|
||||
</li>
|
||||
{isAuthenticated ? (
|
||||
<li
|
||||
className="h-10 w-10 border-2 rounded-full border-indigo-300 text-indigo-200"
|
||||
aria-describedby={popoverId}
|
||||
>
|
||||
<button className="w-full h-full" onClick={togglePopover}>
|
||||
{!loading && (
|
||||
<ImageUploader
|
||||
className="rounded-full object-cover"
|
||||
aspectRatio={1}
|
||||
imageUrl={user?.image}
|
||||
width={80}
|
||||
/>
|
||||
<li className="h-10 w-10">
|
||||
<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"
|
||||
>
|
||||
{!loading && (
|
||||
<ImageFallback
|
||||
width={80}
|
||||
className="rounded-full object-cover"
|
||||
imageId={user?.image}
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="text-lg hover:text-ch-pink-300">
|
||||
Hello {user?.name}
|
||||
</h3>
|
||||
</Link>
|
||||
<hr className="my-2" />
|
||||
<Link
|
||||
className="my-2 mt-4 block hover:text-ch-pink-300"
|
||||
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"
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</button>
|
||||
</Popover>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
@@ -158,38 +182,6 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{isAuthenticated && currentUser && (
|
||||
<Popover
|
||||
id={popoverId}
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={closePopover}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 w-48 text-ch-gray-300">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<h3 className="" style={{ fontWeight: '500' }}>
|
||||
Hello {user?.name}
|
||||
</h3>
|
||||
</Link>
|
||||
<hr />
|
||||
<br />
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<div className="">Your Profile</div>
|
||||
</Link>
|
||||
<a href="#" className="" onClick={logOut}>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
<Toaster timeout={1500} />
|
||||
@@ -197,7 +189,7 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
open={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
/>
|
||||
<main>{children}</main>
|
||||
<main className="flex-grow bg-ch-gray-800">{children}</main>
|
||||
{!shouldRemoveFooterInIde && <Footer />}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -31,15 +31,13 @@ const AccountRecoveryPage = () => {
|
||||
/>
|
||||
|
||||
<section className="max-w-md mx-auto mt-20">
|
||||
<h2 className="text-xl text-indigo-500 pb-4">Send recovery email</h2>
|
||||
<h2 className="text-xl text-ch-gray-300 pb-4">Send recovery email</h2>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<div
|
||||
className="grid items-center gap-2"
|
||||
style={{ gridTemplateColumns: 'auto 1fr' }}
|
||||
>
|
||||
<span className="capitalize text-gray-500 text-sm align-middle my-3">
|
||||
email:
|
||||
</span>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">email</span>
|
||||
<InputTextForm
|
||||
className="text-xl"
|
||||
name="email"
|
||||
@@ -51,10 +49,10 @@ const AccountRecoveryPage = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Submit className="col-start-2 mt-4 bg-ch-purple-400 bg-opacity-50 hover:bg-opacity-80 text-ch-gray-300 flex h-10 flex-shrink-0 justify-center items-center px-4 rounded">
|
||||
Send email
|
||||
</Submit>
|
||||
</div>
|
||||
<Submit className="bg-indigo-200 text-indigo-800 p-2 px-4 shadow hover:shadow-lg mt-4 rounded">
|
||||
Send email
|
||||
</Submit>
|
||||
</Form>
|
||||
</section>
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import MainLayout from 'src/layouts/MainLayout'
|
||||
import EditUserCell from 'src/components/EditUserCell'
|
||||
import Seo from 'src/components/Seo/Seo'
|
||||
|
||||
const UserPage = ({ userName }) => {
|
||||
return (
|
||||
<MainLayout>
|
||||
<>
|
||||
<Seo title={userName} description="Add new project page" lang="en-US" />
|
||||
|
||||
<EditUserCell userName={userName} isEditable />
|
||||
</MainLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ const ProjectsPage = () => {
|
||||
return (
|
||||
<MainLayout shouldRemoveFooterInIde>
|
||||
<Seo
|
||||
title="Home page"
|
||||
title="CadHub Home page"
|
||||
description="Learn about Code CAD and the CadHub community"
|
||||
lang="en-US"
|
||||
socialImageUrl="https://cadhub.xyz/default-social-image.jpg"
|
||||
socialImageUrl="http://cadhub.xyz/default-social-image.jpg"
|
||||
/>
|
||||
<Hero />
|
||||
</MainLayout>
|
||||
|
||||
@@ -8,7 +8,7 @@ const ProjectsPage = () => {
|
||||
<Seo
|
||||
title="Projects page"
|
||||
description="Cadhub Projects page"
|
||||
socialImageUrl="https://cadhub.xyz/default-social-image.jpg"
|
||||
socialImageUrl="http://cadhub.xyz/default-social-image.jpg"
|
||||
lang="en-US"
|
||||
/>
|
||||
<div className="bg-ch-gray-800 pb-64">
|
||||
|
||||
@@ -32,14 +32,14 @@ const UpdatePasswordPage = () => {
|
||||
<Seo title="Update Password" description="Update Password" lang="en-US" />
|
||||
|
||||
<section className="max-w-md mx-auto mt-20">
|
||||
<h2 className="text-xl text-indigo-500 pb-4">Reset Password</h2>
|
||||
<h2 className="text-xl text-ch-gray-300 pb-4">Reset Password</h2>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<div
|
||||
className="grid items-center gap-2"
|
||||
style={{ gridTemplateColumns: 'auto 1fr' }}
|
||||
>
|
||||
<span className="capitalize text-gray-500 text-sm align-middle my-3">
|
||||
password:
|
||||
<span className="capitalize text-ch-gray-300 text-sm">
|
||||
password
|
||||
</span>
|
||||
<InputTextForm
|
||||
className="text-xl"
|
||||
@@ -49,9 +49,7 @@ const UpdatePasswordPage = () => {
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
<span className="capitalize text-gray-500 text-sm align-middle my-3">
|
||||
confirm:
|
||||
</span>
|
||||
<span className="capitalize text-ch-gray-300 text-sm">confirm</span>
|
||||
<InputTextForm
|
||||
className="text-xl"
|
||||
name="confirm"
|
||||
@@ -60,10 +58,10 @@ const UpdatePasswordPage = () => {
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
<Submit className="col-start-2 mt-4 bg-ch-purple-400 bg-opacity-50 hover:bg-opacity-80 text-ch-gray-300 flex h-10 flex-shrink-0 justify-center items-center px-4 rounded">
|
||||
Update
|
||||
</Submit>
|
||||
</div>
|
||||
<Submit className="bg-indigo-200 text-indigo-800 p-2 px-4 shadow hover:shadow-lg mt-4 rounded">
|
||||
Update
|
||||
</Submit>
|
||||
</Form>
|
||||
</section>
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import MainLayout from 'src/layouts/MainLayout'
|
||||
import EditUserCell from 'src/components/EditUserCell'
|
||||
import Seo from 'src/components/Seo/Seo'
|
||||
import { Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
const UserPage = ({ userName }) => {
|
||||
return (
|
||||
<MainLayout>
|
||||
<>
|
||||
<Seo title={userName} description="User page" lang="en-US" />
|
||||
<Toaster timeout={9000} />
|
||||
|
||||
<EditUserCell userName={userName} />
|
||||
</MainLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user