From 55d48057daa149c8cee1343eb46a0fa62013a408 Mon Sep 17 00:00:00 2001 From: Frank Johnson Date: Sun, 12 Sep 2021 05:03:58 -0400 Subject: [PATCH] Initial profile refactor of layout and config --- app/web/config/tailwind.config.js | 3 + app/web/src/components/InputText/InputText.js | 2 +- .../InputTextForm/InputTextForm.tsx | 2 +- app/web/src/components/KeyValue/KeyValue.tsx | 54 +++++ .../components/ProjectCard/ProjectCard.tsx | 5 +- .../ProjectProfile/ProjectProfile.tsx | 49 +---- app/web/src/components/Projects/Projects.tsx | 2 +- .../src/components/UserProfile/UserProfile.js | 113 ----------- .../components/UserProfile/UserProfile.tsx | 112 +++++++++++ .../UserProfile/userProfileConfig.tsx | 189 ++++++++++++++++++ app/web/src/index.css | 13 ++ .../src/pages/EditUserPage/EditUserPage.js | 5 +- app/web/src/pages/UserPage/UserPage.js | 4 +- 13 files changed, 383 insertions(+), 170 deletions(-) create mode 100644 app/web/src/components/KeyValue/KeyValue.tsx delete mode 100644 app/web/src/components/UserProfile/UserProfile.js create mode 100644 app/web/src/components/UserProfile/UserProfile.tsx create mode 100644 app/web/src/components/UserProfile/userProfileConfig.tsx 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 ( - + <> - + ) }