diff --git a/app/web/config/tailwind.config.js b/app/web/config/tailwind.config.js index ea1ef82..7031fa3 100644 --- a/app/web/config/tailwind.config.js +++ b/app/web/config/tailwind.config.js @@ -66,6 +66,9 @@ module.exports = { gridAutoColumns: { 'preview-layout': 'minmax(30rem, 1fr) minmax(auto, 2fr)', }, + gridTemplateColumns: { + 'profile-layout': 'minmax(32rem, 1fr) 2fr', + }, keyframes: { 'bounce-sm': { '0%, 100%': { diff --git a/app/web/src/components/InputText/InputText.js b/app/web/src/components/InputText/InputText.js index 30e447a..4f4cd3b 100644 --- a/app/web/src/components/InputText/InputText.js +++ b/app/web/src/components/InputText/InputText.js @@ -23,7 +23,7 @@ const InputText = ({ )} /> { <>
void + isEditable?: boolean + bottom?: boolean + className?: string +} + +const KeyValue = ({ + keyName, + children, + hide = false, + canEdit = false, + onEdit, + isEditable = false, + bottom = false, + className = "", + } : KeyValueType) => { + if (!children || hide) return null + return ( +
+
+ {keyName} + {canEdit && + (isEditable ? ( + + ) : ( + + ))} +
+
{children}
+
+ ) + } + + export default KeyValue \ No newline at end of file diff --git a/app/web/src/components/ProjectCard/ProjectCard.tsx b/app/web/src/components/ProjectCard/ProjectCard.tsx index 374c1c7..2016b4b 100644 --- a/app/web/src/components/ProjectCard/ProjectCard.tsx +++ b/app/web/src/components/ProjectCard/ProjectCard.tsx @@ -4,6 +4,7 @@ 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 }) => (
  • (
  • - diff --git a/app/web/src/components/ProjectProfile/ProjectProfile.tsx b/app/web/src/components/ProjectProfile/ProjectProfile.tsx index d04835d..b3d1d26 100644 --- a/app/web/src/components/ProjectProfile/ProjectProfile.tsx +++ b/app/web/src/components/ProjectProfile/ProjectProfile.tsx @@ -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 ( -
    -
    - {keyName} - {canEdit && - (isEditable ? ( - - ) : ( - - ))} -
    -
    {children}
    -
    - ) -} +import KeyValue from 'src/components/KeyValue/KeyValue' const ProjectProfile = ({ userProject, @@ -140,7 +96,7 @@ const ProjectProfile = ({
    {/* Side panel */} -
    +

    {project?.title.replace(/-/g, ' ')} @@ -301,3 +257,4 @@ const ProjectProfile = ({ } export default ProjectProfile + diff --git a/app/web/src/components/Projects/Projects.tsx b/app/web/src/components/Projects/Projects.tsx index cbe59a0..b4a31e3 100644 --- a/app/web/src/components/Projects/Projects.tsx +++ b/app/web/src/components/Projects/Projects.tsx @@ -36,7 +36,7 @@ const ProjectsList = ({ return (
      {filteredProjects.map( diff --git a/app/web/src/components/UserProfile/UserProfile.js b/app/web/src/components/UserProfile/UserProfile.js deleted file mode 100644 index 3abc975..0000000 --- a/app/web/src/components/UserProfile/UserProfile.js +++ /dev/null @@ -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 ( - <> -
      -
      - {!isEditable && ( -
      - { - onSave(user.userName, { - ...input, - image, - }) - }} - aspectRatio={1} - isEditable={isImageEditable} - imageUrl={user.image} - width={300} - /> -
      - )} -
      - - setInput({ - ...input, - name, - userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'), - }) - } - isEditable={isEditable} - /> - {isEditable ? ( - // TODO replace pencil with a save icon - ) : canEdit ? ( - - ) : null} -
      -
      -
      -

      Bio:

      -
      - - setInput({ - ...input, - bio: bioFn(), - }) - } - /> -
      -
      -
      -

      Projects:

      - -
      -
      - - ) -} - -export default UserProfile diff --git a/app/web/src/components/UserProfile/UserProfile.tsx b/app/web/src/components/UserProfile/UserProfile.tsx new file mode 100644 index 0000000..4368782 --- /dev/null +++ b/app/web/src/components/UserProfile/UserProfile.tsx @@ -0,0 +1,112 @@ +import { useState, useEffect, useRef, useReducer, ReactNode } 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 { fieldsConfig, fieldReducer, UserProfileType, FieldConfigType } from './userProfileConfig' + +function buildFieldsConfig(fieldsConfig, user) { + Object.entries(fieldsConfig).forEach(([key, field] : [string, FieldConfigType]) => { + field.currentValue = field.newValue = user[key] + field.name = key + }) + + return fieldsConfig +} + + +const UserProfile = ({ + user, + isEditable, + loading, + onSave, + error, + projects, +} : UserProfileType) => { + const { currentUser } = useAuth() + const hasEditPermission = currentUser?.sub === user.id + useEffect(() => { + isEditable && !hasEditPermission && navigate(routes.user({ userName: user.userName })) + }, [currentUser]) + + const initializedFields = buildFieldsConfig(fieldsConfig, user) + const [fields, fieldDispatch] = useReducer(fieldReducer, initializedFields) + + return ( + <> +
      +
      + + + + {}} + projectOwner={user?.userName} + projectOwnerImage={user?.image} + projectOwnerId={user?.id} + /> +
      +
      +
      + {/* Side panel */} +
      +
      + {!isEditable && ( +
      + +
      + )} +
      + + +
      +
      +
      + +
      +
      + +
      +
      + {/* Viewer */} +
      +

      Projects

      + +
      +
      +
      +
      + + ) +} + +export default UserProfile diff --git a/app/web/src/components/UserProfile/userProfileConfig.tsx b/app/web/src/components/UserProfile/userProfileConfig.tsx new file mode 100644 index 0000000..a4c7574 --- /dev/null +++ b/app/web/src/components/UserProfile/userProfileConfig.tsx @@ -0,0 +1,189 @@ +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 interface UserProfileType { + 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, +} + +const ProfileKeyValue = ({ field, dispatch, user, save, hasEditPermission, children, bottom = false }) => { + return ( + { + 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 } + + ) + } + +const bioField : FieldConfigType = { + editable: true, + needsRef: true, + component: (props) => { + const ref = useRef(null) + + const { dispatch, field } = props + + return +
      + e?.target?.id === 'bio-wrap' && + ref?.current?.focusAtEnd() + } + > + dispatch({ type: "SET_NEW_VALUE", payload: { field: field.bio, value: bio() }})} + /> +
      +
      + }, +} + +const createdAtField : FieldConfigType = { + editable: false, + component: (props) => { + const { field } = props + + return +

      { new Date(field.currentValue).toLocaleDateString() }

      +
      + }, +} + +const imageField : FieldConfigType = { + editable: true, + component: (props) => { + const { field, user, save, hasEditPermission } = props + return ( + { + 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 + { (!field.isEditing) + ?

      { user?.name }

      + : dispatch({ type: "SET_NEW_VALUE", payload: { field: field.name, value }})} + isEditable={!field.isEditable} + /> + } +
      + }, +} + +const userNameField : FieldConfigType = { + editable: true, + component: (props) => { + const { dispatch, field } = props + + return + { (!field.isEditing) + ?

      @{ field?.currentValue?.replace(/([^a-zA-Z\d_:])/g, '-') }

      + : dispatch({ type: "SET_NEW_VALUE", payload: { field: field.name, value }})} + isEditable={!field.isEditable} + /> + } +
      + }, +} + +export const fieldsConfig = { + bio: bioField, + createdAt: createdAtField, + id: { + editable: false, + }, + image: imageField, + name: nameField, + updatedAt: { + editable: false, + }, + userName: userNameField, +} + +export function fieldReducer(state, action) { + switch (action.type) { + case "TOGGLE_EDITING": + return { + ...state, + [action.payload]: { + ...state[action.payload], + isEditing: (state[action.payload].editable && !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 + } +} \ No newline at end of file diff --git a/app/web/src/index.css b/app/web/src/index.css index 0b8ef1d..17e159d 100644 --- a/app/web/src/index.css +++ b/app/web/src/index.css @@ -17,6 +17,19 @@ body { 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"; } + + /* custom scrollbar */ + .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; + } } diff --git a/app/web/src/pages/EditUserPage/EditUserPage.js b/app/web/src/pages/EditUserPage/EditUserPage.js index 23623a9..3d52cc3 100644 --- a/app/web/src/pages/EditUserPage/EditUserPage.js +++ b/app/web/src/pages/EditUserPage/EditUserPage.js @@ -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 ( - + <> - + ) } diff --git a/app/web/src/pages/UserPage/UserPage.js b/app/web/src/pages/UserPage/UserPage.js index c2b027b..38072c9 100644 --- a/app/web/src/pages/UserPage/UserPage.js +++ b/app/web/src/pages/UserPage/UserPage.js @@ -4,11 +4,11 @@ import Seo from 'src/components/Seo/Seo' const UserPage = ({ userName }) => { return ( - + <> - + ) }