Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new username screen #838

Merged
merged 8 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
{
"name": "Next.js: debug client-side",
"type": "pwa-chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"env": {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"build": "next build",
"dev": "next dev",
"dev-local": "NEXT_PUBLIC_API_SERVER=http://localhost:4000 yarn dev",
"build-local": "NEXT_PUBLIC_API_SERVER=http://localhost:4000 next build",
"local": "NEXT_PUBLIC_API_SERVER=http://localhost:4000 node ./src/server.js ",
"develop": "next dev",
"start": "next start",
Expand Down Expand Up @@ -156,4 +157,4 @@
"engines": {
"node": "18"
}
}
}
19 changes: 11 additions & 8 deletions src/components/media/UserGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,36 @@ import clx from 'classnames'

import UserMedia from './UserMedia'
import MobileMediaCard from './MobileMediaCard'
import { MediaWithTags, IUserProfile } from '../../js/types'
import { MediaWithTags } from '../../js/types'
import UploadCTA from './UploadCTA'
import { actions } from '../../js/stores'
import SlideViewer from './slideshow/SlideViewer'
import { TinyProfile } from '../users/PublicProfile'
import { WithPermission } from '../../js/types/User'
import { WithPermission, UserPublicPage } from '../../js/types/User'
import { useResponsive } from '../../js/hooks'
import TagList from './TagList'
import InfiniteScroll from 'react-infinite-scroll-component'

export interface UserGalleryProps {
uid: string
userProfile: IUserProfile
initialImageList: MediaWithTags[]
auth: WithPermission
userPublicPage: UserPublicPage
postId: string | null
}

const auth: WithPermission = {
isAuthenticated: false,
isAuthorized: false
}
/**
* Image gallery on user profile.
* Simplifying component Todos:
* - remove bulk taging mode
* - simplify back button logic with Next Layout in v13
*/
export default function UserGallery ({ uid, postId: initialPostId, auth, userProfile, initialImageList }: UserGalleryProps): JSX.Element | null {
export default function UserGallery ({ uid, postId: initialPostId, userPublicPage }: UserGalleryProps): JSX.Element | null {
const router = useRouter()
const imageList = initialImageList
const userProfile = userPublicPage?.profile
const imageList = userPublicPage?.mediaList

const [selectedMediaId, setSlideNumber] = useState<number>(-1)

Expand Down Expand Up @@ -75,7 +78,7 @@ export default function UserGallery ({ uid, postId: initialPostId, auth, userPro
}, [initialPostId, imageList, router])

const onUploadHandler = async (imageUrl: string): Promise<void> => {
await actions.media.addImage(uid, userProfile.uuid, imageUrl, true)
await actions.media.addImage(uid, userProfile.userUuid, imageUrl, true)
}

const imageOnClickHandler = useCallback(async (props: any): Promise<void> => {
Expand Down
24 changes: 11 additions & 13 deletions src/components/media/__tests__/UserGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import type UserGalleryType from '../UserGallery'
import { IUserProfile } from '../../../js/types'
import { mediaList } from './data'
import { UserPublicPage } from '../../../js/types/User'

jest.mock('next/router')

Expand All @@ -26,15 +26,15 @@ const { pushFn, replaceFn } = jest.requireMock('next/router')
const useResponsiveMock = jest.spyOn(useResponsive, 'default')
useResponsiveMock.mockReturnValue({ isDesktop: false, isMobile: true, isTablet: true })

const userProfile: IUserProfile = {
authProviderId: '123',
uuid: '12233455667',
name: 'cat blue',
nick: 'cool_nick_2022',
avatar: 'something',
bio: 'totem eatsum',
roles: [],
loginsCount: 2
const userProfile: UserPublicPage = {
profile: {
userUuid: 'de7a092e-5c3c-445d-a863-b5fbe145e016',
displayName: 'cat blue',
username: 'cool_nick_2022',
avatar: 'https://example.com/avatar.jpg',
bio: 'totem eatsum'
},
mediaList
}

let UserGallery: typeof UserGalleryType
Expand All @@ -55,11 +55,9 @@ describe('Image gallery', () => {

render(
<UserGallery
auth={{ isAuthenticated: false, isAuthorized: false }}
uid={username}
postId={null}
userProfile={userProfile}
initialImageList={mediaList}
userPublicPage={userProfile}
/>)

const images = screen.getAllByRole('img')
Expand Down
19 changes: 14 additions & 5 deletions src/components/ui/form/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface InputProps {
label?: string
labelAlt?: string | JSX.Element
unitLabel?: string
unitLabelPlacement?: 'left' | 'right'
affixClassname?: string
name: string
placeholder?: string
registerOptions?: RegisterOptions
Expand All @@ -13,14 +15,15 @@ interface InputProps {
helper?: string | JSX.Element
disabled?: boolean
readOnly?: boolean
type?: 'text' | 'number'
type?: 'text' | 'number' | 'email'
spellCheck?: boolean
}

type BaseInputProps = InputProps & {
formContext: UseFormReturn
}

export const BaseInput: React.FC<BaseInputProps> = ({ label, name, placeholder = '', className = '', classDefault = INPUT_DEFAULT_CSS, disabled = false, readOnly = false, formContext, registerOptions, type = 'text' }) => {
export const BaseInput: React.FC<BaseInputProps> = ({ label, name, placeholder = '', className = '', classDefault = INPUT_DEFAULT_CSS, affixClassname = AFFIX_DEFAULT_CSS, disabled = false, readOnly = false, formContext, registerOptions, type = 'text', spellCheck = true }) => {
const { register } = formContext
const inputProps = register(name, registerOptions)

Expand All @@ -35,14 +38,15 @@ export const BaseInput: React.FC<BaseInputProps> = ({ label, name, placeholder =
aria-describedby={`${name}-helper`}
disabled={disabled}
readOnly={readOnly}
spellCheck={spellCheck}
/>
)
}

/**
* A reusable react-hook-form input field
*/
export default function Input ({ label, labelAlt, unitLabel, name, registerOptions, placeholder = '', className = '', classDefault = INPUT_DEFAULT_CSS, helper, disabled = false, readOnly = false, type }: InputProps): JSX.Element {
export default function Input ({ label, labelAlt, unitLabel, unitLabelPlacement = 'right', name, registerOptions, placeholder = '', className = '', classDefault = INPUT_DEFAULT_CSS, affixClassname = AFFIX_DEFAULT_CSS, helper, disabled = false, readOnly = false, type, spellCheck = true }: InputProps): JSX.Element {
const formContext = useFormContext()
const { formState: { errors } } = formContext

Expand All @@ -64,9 +68,11 @@ export default function Input ({ label, labelAlt, unitLabel, name, registerOptio
formContext={formContext}
registerOptions={registerOptions}
type={type}
spellCheck={spellCheck}
/>)
: (
<label className='input-group'>
{unitLabelPlacement === 'left' && <span className={affixClassname}>{unitLabel}</span>}
<BaseInput
name={name}
placeholder={placeholder}
Expand All @@ -77,12 +83,13 @@ export default function Input ({ label, labelAlt, unitLabel, name, registerOptio
registerOptions={registerOptions}
formContext={formContext}
type={type}
spellCheck={spellCheck}
/>
<span className='bg-default uppercase text-sm'>{unitLabel}</span>
{unitLabelPlacement === 'right' && <span className={affixClassname}>{unitLabel}</span>}
</label>
)}

<label className='label' id={`${name}-helper`} htmlFor={name}>
<label className='label h-12' id={`${name}-helper`} htmlFor={name}>
{error?.message != null &&
(<span className='label-text-alt text-error'>{error?.message as string}</span>)}
{(error == null) && helper}
Expand All @@ -92,3 +99,5 @@ export default function Input ({ label, labelAlt, unitLabel, name, registerOptio
}

export const INPUT_DEFAULT_CSS = 'input input-primary input-bordered input-md focus:outline-0 focus:ring-1 focus:ring-primary'

const AFFIX_DEFAULT_CSS = 'bg-default uppercase text-sm'
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { KeyIcon } from '@heroicons/react/24/outline'
import { useSession } from 'next-auth/react'

import { WithOwnerProfile } from '../../js/types/User'
import forOwnerOnly from '../../js/auth/forOwnerOnly'
import forOwnerOnly, { ForOwnerOnlyProps } from '../../js/auth/forOwnerOnly'

const APIKey = (props: WithOwnerProfile): JSX.Element | null => {
/**
* A visual button for copying the user's API key to the clipboard.
*/
const APIKeyCopy: React.FC<ForOwnerOnlyProps> = () => {
const { data } = useSession({ required: true })

const apiKey: string | undefined = (data?.accessToken as string) ?? undefined
Expand All @@ -23,4 +25,4 @@ const APIKey = (props: WithOwnerProfile): JSX.Element | null => {
)
}

export default forOwnerOnly<WithOwnerProfile>(APIKey)
export default forOwnerOnly<ForOwnerOnlyProps>(APIKeyCopy)
6 changes: 1 addition & 5 deletions src/components/users/EditProfileButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { Button, ButtonVariant } from '../ui/BaseButton'

import forOwnerOnly from '../../js/auth/forOwnerOnly'
import { WithOwnerProfile } from '../../js/types/User'

interface EditProfileButtonProps extends WithOwnerProfile {
}

function EditProfileButton (): JSX.Element {
return (
<Button href='/account/edit' label='Edit' variant={ButtonVariant.OUTLINED_DEFAULT} size='sm' />
)
}

export default forOwnerOnly<EditProfileButtonProps>(EditProfileButton)
export default forOwnerOnly(EditProfileButton)
49 changes: 26 additions & 23 deletions src/components/users/PublicProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@ import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import ContentLoader from 'react-content-loader'

import { IUserProfile } from '../../js/types/User'
import EditProfileButton from './EditProfileButton'
import { UserPublicProfile } from '../../js/types/User'
// import EditProfileButton from './EditProfileButton'
import ImportFromMtnProj from './ImportFromMtnProj'
import APIKey from './APIKey'
import APIKeyCopy from './APIKeyCopy'
import usePermissions from '../../js/hooks/auth/usePermissions'
import forOwnerOnly from '../../js/auth/forOwnerOnly'

interface PublicProfileProps {
userProfile: IUserProfile
userProfile: UserPublicProfile
onClick?: () => void
}

export default function PublicProfile ({ userProfile: initialUserProfile }: PublicProfileProps): JSX.Element {
const [userProfile, setUserProfile] = useState<IUserProfile | null>(initialUserProfile)
const { isAuthorized } = usePermissions({ ownerProfileOnPage: initialUserProfile })
const [userProfile, setUserProfile] = useState<UserPublicProfile | null>(initialUserProfile)
const { isAuthorized } = usePermissions({ currentUserUuid: userProfile?.userUuid })

useEffect(() => {
setUserProfile(initialUserProfile)
}, [initialUserProfile])

const { name, nick, avatar, bio, website } = userProfile ?? {}
const { displayName, username, bio, website, avatar } = userProfile ?? {}
let websiteWithScheme: string | null = null
if (website != null) {
websiteWithScheme = website.startsWith('http') ? website : `//${website}`
Expand All @@ -34,17 +35,17 @@ export default function PublicProfile ({ userProfile: initialUserProfile }: Publ
: <img className='grayscale object-scale-down w-24 h-24 rounded-full' src={avatar} />}
</div>
<div className='md:col-span-2 text-medium text-primary '>
{nick == null && <TextPlaceholder uniqueKey={123} />}
{username == null && <TextPlaceholder uniqueKey={123} />}

<div className='flex flex-row items-center gap-x-2'>
<div className='text-2xl font-bold mr-4'>
{nick}
{username}
</div>
<EditProfileButton ownerProfile={initialUserProfile} />
{userProfile != null && isAuthorized && <ImportFromMtnProj isButton />}
{userProfile != null && <APIKey ownerProfile={initialUserProfile} />}
{/* <EditProfileButton ownerProfile={initialUserProfile} /> */}
{userProfile != null && <ChangeUsernameLink userUuid={userProfile?.userUuid} />}

</div>
<div className='mt-6 text-lg font-semibold'>{name}</div>
<div className='mt-6 text-lg font-semibold'>{displayName}</div>
<div className=''>{bio}</div>
{websiteWithScheme != null &&
<div className=''>
Expand All @@ -57,14 +58,15 @@ export default function PublicProfile ({ userProfile: initialUserProfile }: Publ
{prettifyUrl(websiteWithScheme)}
</a>
</div>}
<div className='mt-2'>{nick != null
? (
<Link href={`/u2/${nick}`}>
<div className='mt-2 flex items-center gap-2'>
{username != null &&
<Link href={`/u2/${username}`}>
<a className='text-xs'>
<div className='btn btn-info btn-xs'> View ticks</div>
</a>
</Link>)
: null}
</Link>}
{userProfile != null && isAuthorized && <ImportFromMtnProj isButton />}
{userProfile != null && <APIKeyCopy userUuid={userProfile.userUuid} />}
</div>
</div>
</section>
Expand Down Expand Up @@ -99,8 +101,6 @@ const TextPlaceholder = (props): JSX.Element => (
</ContentLoader>
)

// export const PublicProfilePlaceHolder = (): JSX.Element => (<div className='mx-auto max-w-screen-sm px-4 md:px-0 md:grid md:grid-cols-3' />)

/**
* Remove leading http(s):// and trailing /
*/
Expand All @@ -116,17 +116,17 @@ export const TinyProfile = ({ userProfile, onClick }: PublicProfileProps): JSX.E
onClick()
}
}, [])
const { nick, avatar } = userProfile
const { username, avatar } = userProfile
return (

<Link as={`/u/${nick}`} href='/u/[uid]'>
<Link as={`/u/${username}`} href='/u/[uid]'>
<a onClick={onClickHandler}>
<section className='flex items-center space-x-2.5'>
<div className='grayscale'>
<img className='rounded-full' src={avatar} width={32} height={32} />
</div>
<div className={ProfileATagStyle}>
{nick}
{username}
</div>
</section>
</a>
Expand All @@ -148,3 +148,6 @@ export const ProfileATag = ({ uid, className = ProfileATagStyle }: ProfileATagPr
</Link>)

const ProfileATagStyle = 'text-primary font-bold hover:underline'

const ChangeUsernameLink = forOwnerOnly(() =>
<Link href='/account/changeUsername'><a className='text-sm link'>Edit</a></Link>)
Loading
Loading