---
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { Box, Flex, Heading, Image, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import capitalize from 'lodash/capitalize'
|
||||
import { DateTime } from 'luxon'
|
||||
import NextLink from 'next/link'
|
||||
import { AppliedOffersQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeleteOfferButtons } from './EditDeleteOfferButtons'
|
||||
|
||||
interface AppliedOffersProps {
|
||||
me: ReturnType<typeof useUser>
|
||||
appliedOffersData: AppliedOffersQuery
|
||||
appliedOffersFetching: boolean
|
||||
}
|
||||
|
||||
export const AppliedOffers = (props: AppliedOffersProps): JSX.Element => {
|
||||
const { me, appliedOffersData, appliedOffersFetching } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!appliedOffersData && appliedOffersFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
appliedOffersData.appliedOffers.length > 0 &&
|
||||
appliedOffersData.appliedOffers.map(o =>
|
||||
o &&
|
||||
<Flex key={o.id} pl={5} pr={5} pt={3} pb={3} minH='40' shadow='md' borderWidth='1px' bgColor='white'>
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Image src={o.photoUrl} alt='offer-photo' fallbackSrc='/samples/square.webp' w='32' h='32' mr={4} />
|
||||
<Box h='full'>
|
||||
<NextLink passHref href='/offer/[id]' as={`/offer/${o.id}`} >
|
||||
<Link color='green.500' _hover={{ textDecor: 'underline', textDecorationColor: 'green.500' }}>
|
||||
<Heading fontSize='xl'>{o.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Text fontSize='xs' mb={1} color='blackAlpha.600'>Posted by {o.creatorType == 'user' ? o.creator.username : o.pageCreator.pageName} in s/{o.space?.spaceName}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.workplace}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.address}</Text>
|
||||
<Text fontSize='md' mb={1} color={o.recruiting ? 'green.500' : 'red.500'} fontWeight='bold'>{o.recruiting ? 'Recruiting' : 'Not recruiting'}</Text>
|
||||
<Text fontSize='md' mb={1} fontWeight='bold' color={o.applicationStatus == 'applied' ? 'gray' : o.applicationStatus == 'accepted' ? 'green.500' : 'red.500'}>{capitalize(o.applicationStatus)}</Text>
|
||||
<Text fontSize='sm' color='blackAlpha.600'>{DateTime.fromMillis(parseInt(o.createdAt)).toRelative()}</Text>
|
||||
</Box>
|
||||
{
|
||||
((me?.id && me.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeleteOfferButtons id={o.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Flex, Grid, GridItem, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { useAvatarsQuery, useChangeAvatarMutation, UserQuery, useUploadAvatarMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangeAvatarButtonProps {
|
||||
data: UserQuery
|
||||
setAvatarKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangeAvatarButton = (props: ChangeAvatarButtonProps): JSX.Element => {
|
||||
const { data, setAvatarKey } = props
|
||||
const userId = useMemo(() => data?.user?.id, [data])
|
||||
useEffect(() => {
|
||||
setAvatarKey(data?.user?.avatar)
|
||||
}, [data])
|
||||
|
||||
const [{ data: avatarsData }] = useAvatarsQuery({
|
||||
pause: !userId,
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor> | null>(null)
|
||||
const [, uploadAvatar] = useUploadAvatarMutation()
|
||||
const [selected, setSelected] = useState(data?.user?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.user?.avatar)
|
||||
}, [data?.user?.avatar])
|
||||
|
||||
const [, changeAvatar] = useChangeAvatarMutation()
|
||||
|
||||
const { isOpen: isChangingAvatarOpen, onOpen: onChangingAvatarOpen, onClose: onChangingAvatarClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<OutlinedButton position='absolute' top='6' right='110px' h='40px' onClick={onChangingAvatarOpen}>
|
||||
Change Avatar
|
||||
</OutlinedButton>
|
||||
<Modal isOpen={isChangingAvatarOpen} onClose={onChangingAvatarClose} size='2xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change user's avatar</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>User's avatar collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
avatarsData?.avatars?.length > 0 &&
|
||||
avatarsData.avatars.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='avatar'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='40px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<MUIBox sx={{ canvas: { borderRadius: '50%' } }}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={250}
|
||||
height={250}
|
||||
borderRadius={Infinity}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
</MUIBox>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadAvatar({
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setAvatarKey(selected)
|
||||
await changeAvatar({
|
||||
key: selected,
|
||||
})
|
||||
onChangingAvatarClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Box, Button, Flex, Grid, GridItem, Icon, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { useChangeCoverPhotoMutation, useCoverPhotosQuery, UserQuery, useUploadCoverPhotoMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangeCoverPhotoButtonProps {
|
||||
data: UserQuery
|
||||
setCoverPhotoKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangeCoverPhotoButton = (props: ChangeCoverPhotoButtonProps): JSX.Element => {
|
||||
const { data, setCoverPhotoKey } = props
|
||||
const userId = useMemo(() => data?.user?.id, [data])
|
||||
useEffect(() => {
|
||||
setCoverPhotoKey(data?.user?.coverPhoto)
|
||||
}, [data])
|
||||
|
||||
const [{ data: coverPhotosData }] = useCoverPhotosQuery({
|
||||
pause: !userId,
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [, uploadCoverPhoto] = useUploadCoverPhotoMutation()
|
||||
const [selected, setSelected] = useState(data?.user?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.user?.coverPhoto)
|
||||
}, [data?.user?.coverPhoto])
|
||||
|
||||
const [, changeCoverPhoto] = useChangeCoverPhotoMutation()
|
||||
|
||||
const { isOpen: isChangingCoverPhotoOpen, onOpen: onChangingCoverPhotoOpen, onClose: onChangingCoverPhotoClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button borderRadius='full' bgColor='white' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onChangingCoverPhotoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-camera' color='green.500' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isChangingCoverPhotoOpen} onClose={onChangingCoverPhotoClose} size='4xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change user's cover photo</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>User's cover photo collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
coverPhotosData?.coverPhotos?.length > 0 &&
|
||||
coverPhotosData.coverPhotos.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='cover-photo'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='210.5px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={800}
|
||||
height={152}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={0.5}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadCoverPhoto({
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setCoverPhotoKey(selected)
|
||||
await changeCoverPhoto({
|
||||
key: selected,
|
||||
})
|
||||
onChangingCoverPhotoClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Flex, Grid, GridItem, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { PageQuery, useChangePageAvatarMutation, usePageAvatarsQuery, useUploadAvatarMutation, useUploadPageAvatarMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangePageAvatarButtonProps {
|
||||
data: PageQuery
|
||||
setAvatarKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangePageAvatarButton = (props: ChangePageAvatarButtonProps): JSX.Element => {
|
||||
const { data, setAvatarKey } = props
|
||||
const pageId = useMemo(() => data?.page?.id, [data])
|
||||
useEffect(() => {
|
||||
setAvatarKey(data?.page?.avatar)
|
||||
}, [data])
|
||||
|
||||
const [{ data: avatarsData }] = usePageAvatarsQuery({
|
||||
pause: !pageId,
|
||||
variables: {
|
||||
pageId
|
||||
}
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [, uploadAvatar] = useUploadPageAvatarMutation()
|
||||
const [selected, setSelected] = useState(data?.page?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.page?.avatar)
|
||||
}, [data?.page?.avatar])
|
||||
|
||||
const [, changePageAvatar] = useChangePageAvatarMutation()
|
||||
|
||||
const { isOpen: isChangingAvatarOpen, onOpen: onChangingAvatarOpen, onClose: onChangingAvatarClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<OutlinedButton position='absolute' top='6' right='110px' h='40px' onClick={onChangingAvatarOpen}>
|
||||
Change Avatar
|
||||
</OutlinedButton>
|
||||
<Modal isOpen={isChangingAvatarOpen} onClose={onChangingAvatarClose} size='2xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change page's avatar</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>Page's avatar collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
avatarsData?.pageAvatars?.length > 0 &&
|
||||
avatarsData.pageAvatars.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='avatar'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='40px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<MUIBox sx={{ canvas: { borderRadius: '50%' } }}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={250}
|
||||
height={250}
|
||||
borderRadius={Infinity}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
</MUIBox>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadAvatar({
|
||||
pageId,
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setAvatarKey(selected)
|
||||
await changePageAvatar({
|
||||
pageId,
|
||||
key: selected,
|
||||
})
|
||||
onChangingAvatarClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
import { Box, Button, Flex, Grid, GridItem, Icon, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { PageQuery, useChangePageCoverPhotoMutation, usePageCoverPhotosQuery, useUploadPageCoverPhotoMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangePageCoverPhotoButtonProps {
|
||||
data: PageQuery
|
||||
setCoverPhotoKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangePageCoverPhotoButton = (props: ChangePageCoverPhotoButtonProps): JSX.Element => {
|
||||
const { data, setCoverPhotoKey } = props
|
||||
const pageId = useMemo(() => data?.page?.id, [data])
|
||||
useEffect(() => {
|
||||
setCoverPhotoKey(data?.page?.coverPhoto)
|
||||
}, [data])
|
||||
|
||||
const [{ data: coverPhotosData }] = usePageCoverPhotosQuery({
|
||||
pause: !pageId,
|
||||
variables: {
|
||||
pageId
|
||||
}
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [, uploadCoverPhoto] = useUploadPageCoverPhotoMutation()
|
||||
const [selected, setSelected] = useState(data?.page?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.page?.coverPhoto)
|
||||
}, [data?.page?.coverPhoto])
|
||||
|
||||
const [, changeCoverPhoto] = useChangePageCoverPhotoMutation()
|
||||
|
||||
const { isOpen: isChangingCoverPhotoOpen, onOpen: onChangingCoverPhotoOpen, onClose: onChangingCoverPhotoClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button borderRadius='full' bgColor='white' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onChangingCoverPhotoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-camera' color='green.500' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isChangingCoverPhotoOpen} onClose={onChangingCoverPhotoClose} size='4xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change page's cover photo</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>page's cover photo collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
coverPhotosData?.pageCoverPhotos?.length > 0 &&
|
||||
coverPhotosData.pageCoverPhotos.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='cover-photo'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='210.5px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={800}
|
||||
height={152}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={0.5}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadCoverPhoto({
|
||||
pageId,
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setCoverPhotoKey(selected)
|
||||
await changeCoverPhoto({
|
||||
pageId,
|
||||
key: selected,
|
||||
})
|
||||
onChangingCoverPhotoClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
|
||||
import { Flex, Grid, GridItem, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { SpaceQuery, useChangeSpaceAvatarMutation, useSpaceAvatarsQuery, useUploadAvatarMutation, useUploadSpaceAvatarMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangeSpaceAvatarButtonProps {
|
||||
data: SpaceQuery
|
||||
setAvatarKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangeSpaceAvatarButton = (props: ChangeSpaceAvatarButtonProps): JSX.Element => {
|
||||
const { data, setAvatarKey } = props
|
||||
const spaceId = useMemo(() => data?.space?.id, [data])
|
||||
useEffect(() => {
|
||||
setAvatarKey(data?.space?.avatar)
|
||||
}, [data])
|
||||
|
||||
const [{ data: avatarsData }] = useSpaceAvatarsQuery({
|
||||
pause: !spaceId,
|
||||
variables: {
|
||||
spaceId
|
||||
}
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [, uploadAvatar] = useUploadSpaceAvatarMutation()
|
||||
const [selected, setSelected] = useState(data?.space?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.space?.avatar)
|
||||
}, [data?.space?.avatar])
|
||||
|
||||
const [, changeSpaceAvatar] = useChangeSpaceAvatarMutation()
|
||||
|
||||
const { isOpen: isChangingAvatarOpen, onOpen: onChangingAvatarOpen, onClose: onChangingAvatarClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<OutlinedButton position='absolute' top='6' right='110px' h='40px' onClick={onChangingAvatarOpen}>
|
||||
Change Avatar
|
||||
</OutlinedButton>
|
||||
<Modal isOpen={isChangingAvatarOpen} onClose={onChangingAvatarClose} size='2xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change space's avatar</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>Space's avatar collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
avatarsData?.spaceAvatars?.length > 0 &&
|
||||
avatarsData.spaceAvatars.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='avatar'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='40px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<MUIBox sx={{ canvas: { borderRadius: '50%' } }}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={250}
|
||||
height={250}
|
||||
borderRadius={Infinity}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
</MUIBox>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadAvatar({
|
||||
spaceId,
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setAvatarKey(selected)
|
||||
await changeSpaceAvatar({
|
||||
spaceId,
|
||||
key: selected,
|
||||
})
|
||||
onChangingAvatarClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
import { Box, Button, Flex, Grid, GridItem, Icon, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, PropsOf, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { SpaceQuery, useChangeSpaceCoverPhotoMutation, useSpaceCoverPhotosQuery, useUploadSpaceCoverPhotoMutation } from 'src/generated/graphql'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface ChangeSpaceCoverPhotoButtonProps {
|
||||
data: SpaceQuery
|
||||
setCoverPhotoKey: Dispatch<SetStateAction<string | null | undefined>>
|
||||
}
|
||||
|
||||
export const ChangeSpaceCoverPhotoButton = (props: ChangeSpaceCoverPhotoButtonProps): JSX.Element => {
|
||||
const { data, setCoverPhotoKey } = props
|
||||
const spaceId = useMemo(() => data?.space?.id, [data])
|
||||
useEffect(() => {
|
||||
setCoverPhotoKey(data?.space?.coverPhoto)
|
||||
}, [data])
|
||||
|
||||
const [{ data: coverPhotosData }] = useSpaceCoverPhotosQuery({
|
||||
pause: !spaceId,
|
||||
variables: {
|
||||
spaceId
|
||||
}
|
||||
})
|
||||
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [, uploadCoverPhoto] = useUploadSpaceCoverPhotoMutation()
|
||||
const [selected, setSelected] = useState(data?.space?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setSelected(data?.space?.coverPhoto)
|
||||
}, [data?.space?.coverPhoto])
|
||||
|
||||
const [, changeCoverPhoto] = useChangeSpaceCoverPhotoMutation()
|
||||
|
||||
const { isOpen: isChangingCoverPhotoOpen, onOpen: onChangingCoverPhotoOpen, onClose: onChangingCoverPhotoClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button borderRadius='full' bgColor='white' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onChangingCoverPhotoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-camera' color='green.500' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isChangingCoverPhotoOpen} onClose={onChangingCoverPhotoClose} size='4xl'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Change space's cover photo</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text fontWeight='bold' mb={2}>Space's cover photo collection: </Text>
|
||||
<Grid templateColumns='repeat(11, 1fr)' gap='0.25rem 0.5rem'>
|
||||
{
|
||||
coverPhotosData?.spaceCoverPhotos?.length > 0 &&
|
||||
coverPhotosData.spaceCoverPhotos.map((photo, index) => (
|
||||
photo &&
|
||||
<GridItem key={index} >
|
||||
<Flex align='center'>
|
||||
<Image
|
||||
key={index}
|
||||
src={photo.url}
|
||||
alt='cover-photo'
|
||||
onClick={() => {
|
||||
setSelected(photo.key)
|
||||
}}
|
||||
h='40px'
|
||||
w='210.5px'
|
||||
border={selected != photo.key ? '3px solid transparent' : '3px solid green'}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' mt={4}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={800}
|
||||
height={152}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
{...editorOptions}
|
||||
/>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={0.5}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<OutlinedButton
|
||||
onClick={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
await uploadCoverPhoto({
|
||||
spaceId,
|
||||
upload: file
|
||||
})
|
||||
setEditorOptions(null)
|
||||
}}
|
||||
mt={4}
|
||||
>
|
||||
Save to collection
|
||||
</OutlinedButton>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
marginTop: '1rem',
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => { setEditorOptions({ image: acceptedFiles[0] }) }}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton onClick={async () => {
|
||||
setCoverPhotoKey(selected)
|
||||
await changeCoverPhoto({
|
||||
spaceId,
|
||||
key: selected,
|
||||
})
|
||||
onChangingCoverPhotoClose()
|
||||
}}
|
||||
baseColorLevel={500}
|
||||
>
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons'
|
||||
import { Flex, IconButton, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { RegularCommentFragment, useVoteCommentMutation } from 'src/generated/graphql'
|
||||
import { useRequireLogin } from 'src/hooks'
|
||||
|
||||
interface CommentVoteSectionProps {
|
||||
comment: RegularCommentFragment
|
||||
}
|
||||
|
||||
export const CommentVoteSection = (props: CommentVoteSectionProps): JSX.Element => {
|
||||
const { comment: c } = props
|
||||
const [loadingState, setLoadingState] = useState<'upvote-loading' | 'downvote-loading' | 'not-loading'>
|
||||
('not-loading')
|
||||
const [, voteComment] = useVoteCommentMutation()
|
||||
const requireLogin = useRequireLogin()
|
||||
return (
|
||||
<Flex alignItems='center' ml='-3'>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setLoadingState('upvote-loading')
|
||||
requireLogin()
|
||||
await voteComment({
|
||||
commentId: c.id,
|
||||
value: 1,
|
||||
})
|
||||
setLoadingState('not-loading')
|
||||
}}
|
||||
isLoading={loadingState == 'upvote-loading'}
|
||||
icon={
|
||||
<ArrowUpIcon boxSize='22px' />
|
||||
}
|
||||
color={c.voteStatus == 1 ? 'green' : ''}
|
||||
bg='transparent'
|
||||
aria-label='upvote'
|
||||
/>
|
||||
<Text ml={2} mr={2}>{c.points}</Text>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setLoadingState('downvote-loading')
|
||||
requireLogin()
|
||||
await voteComment({
|
||||
commentId: c.id,
|
||||
value: -1,
|
||||
})
|
||||
setLoadingState('not-loading')
|
||||
}}
|
||||
isLoading={loadingState == 'downvote-loading'}
|
||||
icon={
|
||||
<ArrowDownIcon boxSize='22px' />
|
||||
}
|
||||
color={c.voteStatus == -1 ? 'tomato' : ''}
|
||||
bg='transparent'
|
||||
aria-label='downvote'
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react'
|
||||
import { DataType } from 'csstype'
|
||||
|
||||
type ContainedButtonProps = ButtonProps & {
|
||||
color?: DataType.NamedColor
|
||||
baseColorLevel?: 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
|
||||
}
|
||||
|
||||
export const ContainedButton = (props: ContainedButtonProps): JSX.Element => {
|
||||
const { color = 'green', children, baseColorLevel = 400 } = props
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
bgColor={`${color}.${baseColorLevel}`}
|
||||
textColor='white'
|
||||
_hover={{ bgColor: `${color}.${parseInt(baseColorLevel as string) - 100}` }}
|
||||
_active={{ bgColor: `${color}.${parseInt(baseColorLevel as string) + 100}` }}
|
||||
_focusVisible={{ boxShadow: 'none' }}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react'
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import { CreatableSelect } from 'chakra-react-select'
|
||||
import { useField } from 'formik'
|
||||
import { SelectOption } from 'src/types'
|
||||
|
||||
type CreatableSelectFieldProps = PropsOf<typeof CreatableSelect> & {
|
||||
label: string
|
||||
name: string
|
||||
defaultValue?: SelectOption | SelectOption[]
|
||||
}
|
||||
|
||||
export const CreatableSelectField = (props: CreatableSelectFieldProps): JSX.Element => {
|
||||
const { label, name, defaultValue, ...rest } = props
|
||||
const [field, { error }, { setValue }] = useField({ name })
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<CreatableSelect
|
||||
isMulti
|
||||
useBasicStyles
|
||||
colorScheme='green'
|
||||
selectedOptionColor='green'
|
||||
focusBorderColor='green.500'
|
||||
defaultValue={defaultValue}
|
||||
onChange={(newValue: SelectOption[]) => { setValue(newValue.map(v => v.value)) }}
|
||||
{...props}
|
||||
/>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { PlusSquareIcon } from '@chakra-ui/icons'
|
||||
import { Button, Flex, IconButton, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Dispatch, useEffect, useState } from 'react'
|
||||
import { useCreateConversationMutation } from 'src/generated/graphql'
|
||||
import { toErrorMap } from 'src/utils'
|
||||
import { FormSuccessMessage } from './FormSuccessMessage'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface CreateConversationButtonProps {
|
||||
setConversationId: Dispatch<string>
|
||||
}
|
||||
|
||||
export const CreateConversationButton = (props: CreateConversationButtonProps): JSX.Element => {
|
||||
const { setConversationId } = props
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [, createConversation] = useCreateConversationMutation()
|
||||
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (showSuccessMessage) {
|
||||
setTimeout(() => {
|
||||
setShowSuccessMessage(false)
|
||||
}, 500)
|
||||
}
|
||||
}, [showSuccessMessage])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<IconButton onClick={onOpen} aria-label='create-conversation' bgColor='transparent' _focusVisible={{ boxShadow: '0 0 0 3px #68D391;' }} icon={<PlusSquareIcon color='green' fontSize='lg' />} />
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<Formik
|
||||
initialValues={{ partnerUsername: '' }}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await createConversation(values)
|
||||
if (response.data?.createConversation.errors) {
|
||||
setErrors(toErrorMap(response.data.createConversation.errors))
|
||||
}
|
||||
else {
|
||||
setShowSuccessMessage(true)
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
setConversationId(response.data.createConversation.conversation.id)
|
||||
}, 550)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Create new conversation</ModalHeader>
|
||||
<ModalCloseButton _focusVisible={{ boxShadow: '0 0 0 3px #68D391;' }} />
|
||||
<ModalBody>
|
||||
<InputField name='partnerUsername' placeholder='username' label='With:' />
|
||||
{
|
||||
showSuccessMessage &&
|
||||
<FormSuccessMessage message='Conversation created successfully.' />
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex justify='center' align='center' w='full'>
|
||||
<Button colorScheme='green' type='submit' isLoading={isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useCreateEducationItemMutation } from 'src/generated/graphql'
|
||||
import { toErrorMap } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { PhotoField } from './PhotoField'
|
||||
|
||||
interface CreateEducationItemButtonProps {
|
||||
|
||||
}
|
||||
|
||||
export const CreateEducationItemButton = (props: CreateEducationItemButtonProps): JSX.Element => {
|
||||
const { } = props
|
||||
const [, createEducationItem] = useCreateEducationItemMutation()
|
||||
const { isOpen: isAddingEducationItemOpen, onOpen: onAddingEducationItemOpen, onClose: onAddingEducationItemClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onAddingEducationItemOpen} variant='ghost' w='40px' h='40px' position='absolute' top={10} right='3rem' >
|
||||
<Icon as={() => <Box className='fa-solid fa-plus' fontSize='1.25rem' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isAddingEducationItemOpen} onClose={onAddingEducationItemClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ school: '', status: '', startDate: null, endDate: null, photo: null }}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await createEducationItem({ input: values })
|
||||
if (response.data?.createEducationItem?.errors) {
|
||||
setErrors(toErrorMap(response.data.createEducationItem.errors))
|
||||
}
|
||||
else {
|
||||
onAddingEducationItemClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add education</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='school' placeholder='' label='School' />
|
||||
<Box mt={4}>
|
||||
<InputField name='status' placeholder='e.g. Graduated, dropped out, etc.' label='Status' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Start Date' name='startDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='End Date' name='endDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useCreateExperienceMutation } from 'src/generated/graphql'
|
||||
import { toErrorMap } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { PhotoField } from './PhotoField'
|
||||
|
||||
interface CreateExperienceButtonProps {
|
||||
|
||||
}
|
||||
|
||||
export const CreateExperienceButton = (props: CreateExperienceButtonProps): JSX.Element => {
|
||||
const { } = props
|
||||
const [, createExperience] = useCreateExperienceMutation()
|
||||
const { isOpen: isAddingExperienceOpen, onOpen: onAddingExperienceOpen, onClose: onAddingExperienceClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onAddingExperienceOpen} variant='ghost' w='40px' h='40px' position='absolute' top={10} right='3rem' >
|
||||
<Icon as={() => <Box className='fa-solid fa-plus' fontSize='1.25rem' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isAddingExperienceOpen} onClose={onAddingExperienceClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ title: '', workplace: '', startDate: null, endDate: null, photo: null }}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await createExperience({ input: values })
|
||||
if (response.data?.createExperience?.errors) {
|
||||
setErrors(toErrorMap(response.data.createExperience.errors))
|
||||
}
|
||||
else {
|
||||
onAddingExperienceClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add experience</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='title' placeholder='e.g. Intern, CEO, etc.' label='Title' />
|
||||
<Box mt={4}>
|
||||
<InputField name='workplace' placeholder='' label='Workplace' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Start Date' name='startDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='End Date' name='endDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { QualificationInput, useCreateQualificationMutation } from 'src/generated/graphql'
|
||||
import { toErrorMap } from 'src/utils'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { PhotoField } from './PhotoField'
|
||||
import { SwitchField } from './SwitchField'
|
||||
|
||||
interface CreateQualificationButtonProps {
|
||||
|
||||
}
|
||||
|
||||
export const CreateQualificationButton = (props: CreateQualificationButtonProps): JSX.Element => {
|
||||
const { } = props
|
||||
const [, createQualification] = useCreateQualificationMutation()
|
||||
const { isOpen: isAddingQualificationOpen, onOpen: onAddingQualificationOpen, onClose: onAddingQualificationClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onAddingQualificationOpen} variant='ghost' w='40px' h='40px' position='absolute' top={10} right='3rem' >
|
||||
<Icon as={() => <Box className='fa-solid fa-plus' fontSize='1.25rem' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isAddingQualificationOpen} onClose={onAddingQualificationClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ name: '', issuingOrganisation: '', issuanceDate: null, expire: false, expirationDate: null, credentialID: '', credentialURL: '', photo: null } as QualificationInput}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await createQualification({ input: values })
|
||||
if (response.data?.createQualification?.errors) {
|
||||
setErrors(toErrorMap(response.data.createQualification.errors))
|
||||
}
|
||||
else {
|
||||
onAddingQualificationClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{'Add license or certification'}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='name' placeholder='' label='Name' />
|
||||
<Box mt={4}>
|
||||
<InputField name='issuingOrganisation' placeholder='' label='Issuing Organisation' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Issuance Date' name='issuanceDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<SwitchField label='Expire' name='expire' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Expiration Date' name='expirationDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='credentialID' placeholder='' label='Credential ID' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='credentialURL' placeholder='' label='Credential URL' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react'
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import { DatePicker } from '@mantine/dates'
|
||||
import { useField } from 'formik'
|
||||
import { useRef } from 'react'
|
||||
|
||||
type DatePickerFieldProps = PropsOf<typeof DatePicker> & {
|
||||
label: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const DatePickerField = (props: DatePickerFieldProps): JSX.Element => {
|
||||
const { label, name, ...rest } = props
|
||||
const [field, { error }, { setValue }] = useField({ name })
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<DatePicker
|
||||
value={field.value ? new Date(field.value) : null}
|
||||
onChange={(value) => { setValue((new Date(value)).toISOString()) }}
|
||||
ref={ref}
|
||||
zIndex={1401}
|
||||
dropdownPosition='flip'
|
||||
classNames={{
|
||||
dropdown: `${name}-datePicker-dropdown`,
|
||||
wrapper: `${name}-datePicker-wrapper`,
|
||||
yearPickerControlActive: `${name}-datePicker-yearPickerControlActive`,
|
||||
monthPickerControlActive: `${name}-datePicker-monthPickerControlActive`,
|
||||
day: `${name}-datePicker-day`,
|
||||
}}
|
||||
styles={{
|
||||
dropdown: { marginLeft: `${ref?.current?.clientWidth ? (ref.current.clientWidth - 300) / 2 : 0}px` },
|
||||
wrapper: {
|
||||
input: {
|
||||
'&:focus-within': {
|
||||
borderColor: '#48BB78',
|
||||
boxShadow: '0 0 0 1px #48BB78',
|
||||
}
|
||||
}
|
||||
},
|
||||
yearPickerControlActive: { backgroundColor: '#48BB78', '&:hover': { backgroundColor: '#48BB78' } },
|
||||
monthPickerControlActive: { backgroundColor: '#48BB78', '&:hover': { backgroundColor: '#48BB78' } },
|
||||
day: { '&[data-selected]': { backgroundColor: '#48BB78' } }
|
||||
}}
|
||||
id={field.name}
|
||||
{...props}
|
||||
label={null}
|
||||
/>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { UserQuery, useUpdateInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface EditAboutButtonProps {
|
||||
data: UserQuery
|
||||
}
|
||||
|
||||
export const EditAboutButton = (props: EditAboutButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdateInfoMutation()
|
||||
const { isOpen: isEditingAboutOpen, onOpen: onEditingAboutOpen, onClose: onEditingAboutClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top='10' right='10' onClick={onEditingAboutOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingAboutOpen} onClose={onEditingAboutClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ about: data?.user?.about }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ input: values })
|
||||
onEditingAboutClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update about</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='about' placeholder='A full introduction about yourself' label='' inputType='quill' />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { Flex, IconButton, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { Comment, useDeleteCommentMutation, useUpdateCommentMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface EditDeleteCommentButtonsProps {
|
||||
comment: Partial<Comment>
|
||||
}
|
||||
|
||||
export const EditDeleteCommentButtons = (props: EditDeleteCommentButtonsProps): JSX.Element => {
|
||||
const { comment } = props
|
||||
const [, deleteComment] = useDeleteCommentMutation()
|
||||
const [, updateComment] = useUpdateCommentMutation()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
color='brown'
|
||||
bg='transparent'
|
||||
icon={<EditIcon />}
|
||||
aria-label='edit'
|
||||
onClick={onOpen}
|
||||
/>
|
||||
<Modal isOpen={isOpen} onClose={onClose} size='xl'>
|
||||
<Formik
|
||||
initialValues={{ id: comment.id, text: comment.text }}
|
||||
onSubmit={async values => {
|
||||
await updateComment(values)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form onChange={() => { }}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Comment</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='text' placeholder='comment...' label='' inputType='textarea' />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
<IconButton
|
||||
color='red.500'
|
||||
bg='transparent'
|
||||
icon={<DeleteIcon />}
|
||||
aria-label='delete'
|
||||
onClick={async () => {
|
||||
await deleteComment({ id: comment.id })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { IconButton } from '@chakra-ui/react'
|
||||
import NextLink from 'next/link'
|
||||
import { useDeleteOfferMutation } from 'src/generated/graphql'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
|
||||
interface EditDeleteOfferButtonsProps {
|
||||
id: number
|
||||
}
|
||||
|
||||
export const EditDeleteOfferButtons = (props: EditDeleteOfferButtonsProps): JSX.Element => {
|
||||
const { id } = props
|
||||
const [, deleteOffer] = useDeleteOfferMutation()
|
||||
return (
|
||||
<>
|
||||
<NextLink passHref href='/offer/edit/[id]' as={`/offer/edit/${id}`}>
|
||||
<IconButton
|
||||
color='brown'
|
||||
bg='transparent'
|
||||
icon={<EditIcon />}
|
||||
aria-label='edit'
|
||||
as={NoUnderlineLink}
|
||||
/>
|
||||
</NextLink>
|
||||
<IconButton
|
||||
color='red.500'
|
||||
bg='transparent'
|
||||
icon={<DeleteIcon />}
|
||||
aria-label='delete'
|
||||
onClick={async () => {
|
||||
await deleteOffer({ id })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { IconButton } from '@chakra-ui/react'
|
||||
import NextLink from 'next/link'
|
||||
import { Page, useDeletePageMutation } from 'src/generated/graphql'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
|
||||
interface EditDeletePageButtonsProps {
|
||||
page: Partial<Page>
|
||||
}
|
||||
|
||||
export const EditDeletePageButtons = (props: EditDeletePageButtonsProps): JSX.Element => {
|
||||
const { page } = props
|
||||
const [, deletePage] = useDeletePageMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextLink passHref href='/p/edit/[page]' as={`/p/edit/${page.pageName}`}>
|
||||
<IconButton
|
||||
color='brown'
|
||||
bg='transparent'
|
||||
icon={<EditIcon />}
|
||||
aria-label='edit'
|
||||
as={NoUnderlineLink}
|
||||
/>
|
||||
</NextLink>
|
||||
<IconButton
|
||||
color='red.500'
|
||||
bg='transparent'
|
||||
icon={<DeleteIcon />}
|
||||
aria-label='delete'
|
||||
onClick={async () => {
|
||||
await deletePage({ id: page.id })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { IconButton } from '@chakra-ui/react'
|
||||
import NextLink from 'next/link'
|
||||
import { useDeletePostMutation } from 'src/generated/graphql'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
|
||||
interface EditDeletePostButtonsProps {
|
||||
id: number
|
||||
}
|
||||
|
||||
export const EditDeletePostButtons = (props: EditDeletePostButtonsProps): JSX.Element => {
|
||||
const { id } = props
|
||||
const [, deletePost] = useDeletePostMutation()
|
||||
return (
|
||||
<>
|
||||
<NextLink passHref href='/post/edit/[id]' as={`/post/edit/${id}`}>
|
||||
<IconButton
|
||||
color='brown'
|
||||
bg='transparent'
|
||||
icon={<EditIcon />}
|
||||
aria-label='edit'
|
||||
as={NoUnderlineLink}
|
||||
/>
|
||||
</NextLink>
|
||||
<IconButton
|
||||
color='red.500'
|
||||
bg='transparent'
|
||||
icon={<DeleteIcon />}
|
||||
aria-label='delete'
|
||||
onClick={async () => {
|
||||
await deletePost({ id })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { IconButton } from '@chakra-ui/react'
|
||||
import NextLink from 'next/link'
|
||||
import { Space, useDeleteSpaceMutation } from 'src/generated/graphql'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
|
||||
interface EditDeleteSpaceButtonsProps {
|
||||
space: Partial<Space>
|
||||
}
|
||||
|
||||
export const EditDeleteSpaceButtons = (props: EditDeleteSpaceButtonsProps): JSX.Element => {
|
||||
const { space } = props
|
||||
const [, deleteSpace] = useDeleteSpaceMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextLink passHref href='/s/edit/[space]' as={`/s/edit/${space.spaceName}`}>
|
||||
<IconButton
|
||||
color='brown'
|
||||
bg='transparent'
|
||||
icon={<EditIcon />}
|
||||
aria-label='edit'
|
||||
as={NoUnderlineLink}
|
||||
/>
|
||||
</NextLink>
|
||||
<IconButton
|
||||
color='red.500'
|
||||
bg='transparent'
|
||||
icon={<DeleteIcon />}
|
||||
aria-label='delete'
|
||||
onClick={async () => {
|
||||
await deleteSpace({ id: space.id })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { PageQuery, useUpdatePageInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface EditPageAboutButtonProps {
|
||||
data: PageQuery
|
||||
}
|
||||
|
||||
export const EditPageAboutButton = (props: EditPageAboutButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdatePageInfoMutation()
|
||||
const { isOpen: isEditingAboutOpen, onOpen: onEditingAboutOpen, onClose: onEditingAboutClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top='10' right='10' onClick={onEditingAboutOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingAboutOpen} onClose={onEditingAboutClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ about: data?.page?.about }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ id: data?.page?.id, input: values })
|
||||
onEditingAboutClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update about</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='about' placeholder='A full introduction about your page' label='' inputType='quill' />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { SpaceQuery, useUpdateSpaceInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface EditSpaceAboutButtonProps {
|
||||
data: SpaceQuery
|
||||
}
|
||||
|
||||
export const EditSpaceAboutButton = (props: EditSpaceAboutButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdateSpaceInfoMutation()
|
||||
const { isOpen: isEditingAboutOpen, onOpen: onEditingAboutOpen, onClose: onEditingAboutClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top='10' right='10' onClick={onEditingAboutOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingAboutOpen} onClose={onEditingAboutClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ about: data?.space?.about }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ id: data?.space?.id, input: values })
|
||||
onEditingAboutClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update about</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='about' placeholder='A full introduction about your space' label='' inputType='quill' />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { SpaceQuery, useUpdateSpaceInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface EditSpaceRulesButtonProps {
|
||||
data: SpaceQuery
|
||||
}
|
||||
|
||||
export const EditSpaceRulesButton = (props: EditSpaceRulesButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdateSpaceInfoMutation()
|
||||
const { isOpen: isEditingRulesOpen, onOpen: onEditingRulesOpen, onClose: onEditingRulesClose } = useDisclosure()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top='10' right='10' onClick={onEditingRulesOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingRulesOpen} onClose={onEditingRulesClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ rules: data?.space?.rules }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ id: data?.space?.id, input: values })
|
||||
onEditingRulesClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update rules</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='rules' placeholder='Some rules for posting at your space' label='' inputType='quill' />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Box, Button, Flex, GridItem, Heading, Icon, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { DateTime } from 'luxon'
|
||||
import { EducationItemsQuery, MeQuery, useDeleteEducationItemMutation, useUpdateEducationItemMutation, useUserQuery } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { Interpunct } from './Interpunct'
|
||||
import { PhotoField } from './PhotoField'
|
||||
|
||||
interface EducationItemProps {
|
||||
data: ReturnType<typeof useUserQuery>[0]['data']
|
||||
me: MeQuery['me']
|
||||
item: EducationItemsQuery['educationItems'][0]
|
||||
}
|
||||
|
||||
export const EducationItem = (props: EducationItemProps): JSX.Element => {
|
||||
const { data, me, item } = props
|
||||
const { isOpen: isEditingEducationItemOpen, onOpen: onEditingEducationItemOpen, onClose: onEditingEducationItemClose } = useDisclosure()
|
||||
const [, updateEducationItem] = useUpdateEducationItemMutation()
|
||||
const [, deleteEducationItem] = useDeleteEducationItemMutation()
|
||||
|
||||
return (
|
||||
<GridItem key={item.id}>
|
||||
<Flex align='center' minH='62px'>
|
||||
<Image src={item.photoUrl} alt='education-item-photo' fallbackSrc='/samples/square.webp' w='45px' h='45px' mr={4} />
|
||||
<Box>
|
||||
<Heading size='sm'>{item.school}</Heading>
|
||||
<Text color='gray.600' fontSize='sm'>{item?.status}</Text>
|
||||
<Flex color='gray.600' fontSize='smaller'>
|
||||
{
|
||||
item.startDate &&
|
||||
DateTime.fromISO(item.startDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)
|
||||
}
|
||||
{
|
||||
item.startDate && item.endDate &&
|
||||
<Interpunct />
|
||||
}
|
||||
{
|
||||
item.endDate &&
|
||||
DateTime.fromISO(item.endDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)
|
||||
}
|
||||
</Flex>
|
||||
</Box>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<Box ml='auto'>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={onEditingEducationItemOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingEducationItemOpen} onClose={onEditingEducationItemClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ school: item.school, status: item.status, startDate: item.startDate, endDate: item.endDate, photo: null }}
|
||||
onSubmit={async values => {
|
||||
await updateEducationItem({ id: item.id, input: values })
|
||||
onEditingEducationItemClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update education</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='school' placeholder='' label='School' />
|
||||
<Box mt={4}>
|
||||
<InputField name='status' placeholder='e.g. Graduated, dropped out, etc.' label='Status' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Start Date' name='startDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='End Date' name='endDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={async () => { await deleteEducationItem({ id: item.id }) }}>
|
||||
<Icon as={() => <Box className='fa-regular fa-trash' />} />
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</Flex>
|
||||
</GridItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Box, Button, Flex, GridItem, Heading, Icon, Image, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { DateTime } from 'luxon'
|
||||
import { ExperiencesQuery, MeQuery, useDeleteExperienceMutation, useUpdateExperienceMutation, useUserQuery } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { Interpunct } from './Interpunct'
|
||||
import { PhotoField } from './PhotoField'
|
||||
|
||||
interface ExperienceProps {
|
||||
data: ReturnType<typeof useUserQuery>[0]['data']
|
||||
me: MeQuery['me']
|
||||
item: ExperiencesQuery['experiences'][0]
|
||||
}
|
||||
|
||||
export const Experience = (props: ExperienceProps): JSX.Element => {
|
||||
const { data, me, item } = props
|
||||
const { isOpen: isEditingExperienceOpen, onOpen: onEditingExperienceOpen, onClose: onEditingExperienceClose } = useDisclosure()
|
||||
const [, updateExperience] = useUpdateExperienceMutation()
|
||||
const [, deleteExperience] = useDeleteExperienceMutation()
|
||||
|
||||
return (
|
||||
<GridItem key={item.id}>
|
||||
<Flex align='center' minH='62px'>
|
||||
<Image src={item.photoUrl} alt='experience-photo' fallbackSrc='/samples/square.webp' w='45px' h='45px' mr={4} />
|
||||
<Box>
|
||||
<Heading size='sm'>{item.title}</Heading>
|
||||
<Text color='gray.600' fontSize='sm'>{item?.workplace}</Text>
|
||||
<Flex color='gray.600' fontSize='smaller'>
|
||||
{
|
||||
item.startDate &&
|
||||
DateTime.fromISO(item.startDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)
|
||||
}
|
||||
{
|
||||
item.startDate && item.endDate &&
|
||||
<Interpunct />
|
||||
}
|
||||
{
|
||||
item.endDate &&
|
||||
DateTime.fromISO(item.endDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)
|
||||
}
|
||||
</Flex>
|
||||
</Box>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<Box ml='auto'>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={onEditingExperienceOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingExperienceOpen} onClose={onEditingExperienceClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ title: item.title, workplace: item.workplace, startDate: item.startDate, endDate: item.endDate, photo: null }}
|
||||
onSubmit={async values => {
|
||||
await updateExperience({ id: item.id, input: values })
|
||||
onEditingExperienceClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update experience</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='title' placeholder='e.g. Intern, CEO, etc.' label='Title' />
|
||||
<Box mt={4}>
|
||||
<InputField name='workplace' placeholder='' label='Workplace' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Start Date' name='startDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='End Date' name='endDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={async () => { await deleteExperience({ id: item.id }) }}>
|
||||
<Icon as={() => <Box className='fa-regular fa-trash' />} />
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</Flex>
|
||||
</GridItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { FormControl, FormLabel, FormErrorMessage, Tag, TagCloseButton, TagLabel, Text } from '@chakra-ui/react'
|
||||
import { useField } from 'formik'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface FileFieldProps {
|
||||
label: string
|
||||
name: string
|
||||
values: any
|
||||
}
|
||||
|
||||
export const FileField = (props: FileFieldProps): JSX.Element => {
|
||||
const { label, name, values, ...rest } = props
|
||||
const [field, { error, }, { setValue, }] = useField({ name })
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
{
|
||||
values[`${name}`] ?
|
||||
<Tag
|
||||
borderRadius='full'
|
||||
variant='subtle'
|
||||
colorScheme='green'
|
||||
>
|
||||
<TagLabel>{values[`${name}`].name}</TagLabel>
|
||||
<TagCloseButton onClick={() => { setValue(null) }} />
|
||||
</Tag>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => {
|
||||
setValue(acceptedFiles[0])
|
||||
}}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your file here ...' :
|
||||
'Drag \'n\' drop a file here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react'
|
||||
import { PageQuery, useFollowMutation, useUnfollowMutation, useFollowStatusQuery } from 'src/generated/graphql'
|
||||
import { useRequireLogin } from 'src/hooks'
|
||||
|
||||
type FollowPageButtonProps = ButtonProps & {
|
||||
data: PageQuery
|
||||
}
|
||||
|
||||
export const FollowPageButton = (props: FollowPageButtonProps): JSX.Element => {
|
||||
const { data, ...rest } = props
|
||||
const requireLogin = useRequireLogin()
|
||||
const [, followPage] = useFollowMutation()
|
||||
const [, unfollowPage] = useUnfollowMutation()
|
||||
const [{ data: pageFollowStatusData }] = useFollowStatusQuery({
|
||||
pause: !data?.page?.id,
|
||||
variables: {
|
||||
pageId: data?.page?.id
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
position='absolute'
|
||||
color={pageFollowStatusData?.followStatus ? 'red.500' : 'green.500'}
|
||||
onClick={async () => {
|
||||
requireLogin()
|
||||
if (pageFollowStatusData?.followStatus) {
|
||||
await unfollowPage({ pageId: data?.page?.id })
|
||||
}
|
||||
else {
|
||||
await followPage({ pageId: data?.page?.id })
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
pageFollowStatusData?.followStatus ? 'unfollow' : 'follow'
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button } from '@chakra-ui/react'
|
||||
import { UserQuery, useFollowUserMutation, useUnfollowUserMutation, useUserFollowStatusQuery } from 'src/generated/graphql'
|
||||
import { useRequireLogin } from 'src/hooks'
|
||||
|
||||
interface FollowUserButtonProps {
|
||||
data: UserQuery
|
||||
}
|
||||
|
||||
export const FollowUserButton = (props: FollowUserButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const requireLogin = useRequireLogin()
|
||||
const [, followUser] = useFollowUserMutation()
|
||||
const [, unfollowUser] = useUnfollowUserMutation()
|
||||
const [{ data: userFollowStatusData }] = useUserFollowStatusQuery({
|
||||
pause: !data?.user?.id,
|
||||
variables: {
|
||||
id: data?.user?.id
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
mr={4}
|
||||
ml='auto'
|
||||
color={userFollowStatusData?.userFollowStatus ? 'red.500' : 'green.500'}
|
||||
onClick={async () => {
|
||||
requireLogin()
|
||||
if (userFollowStatusData?.userFollowStatus) {
|
||||
await unfollowUser({ id: data?.user?.id })
|
||||
}
|
||||
else {
|
||||
await followUser({ id: data?.user?.id })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{
|
||||
userFollowStatusData?.userFollowStatus ? 'unfollow' : 'follow'
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CheckIcon } from '@chakra-ui/icons'
|
||||
import { Flex, Box } from '@chakra-ui/react'
|
||||
import { formSuccessMessage } from 'src/styles'
|
||||
|
||||
interface FormSuccessMessageProps {
|
||||
message: string
|
||||
}
|
||||
|
||||
export const FormSuccessMessage = (props: FormSuccessMessageProps): JSX.Element => {
|
||||
const { message } = props
|
||||
return (
|
||||
<Flex alignItems='center' style={formSuccessMessage}>
|
||||
<CheckIcon color='green.700' fontSize='0.875rem' mr={2} />
|
||||
<Box>{message}</Box>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Flex, Avatar, IconButton, Text, Box, Popover, PopoverArrow, PopoverBody, PopoverCloseButton, PopoverContent, PopoverHeader, PopoverTrigger, Button, VStack, useDisclosure, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'
|
||||
import { Dispatch, useState } from 'react'
|
||||
import { InboxesQuery, useDeleteConversationMutation } from 'src/generated/graphql'
|
||||
import { FormSuccessMessage } from './FormSuccessMessage'
|
||||
import { InputField } from './InputField'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
|
||||
interface InboxItemProps {
|
||||
conversationId: string
|
||||
setConversationId: Dispatch<string>
|
||||
data: InboxesQuery
|
||||
inbox: InboxesQuery['inboxes'][0]
|
||||
index: number
|
||||
}
|
||||
|
||||
export const InboxItem = (props: InboxItemProps): JSX.Element => {
|
||||
const { data, setConversationId, inbox, conversationId, index } = props
|
||||
const [showEllipsis, setShowEllipsis] = useState(false)
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [, deleteConversation] = useDeleteConversationMutation()
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onClick={() => {
|
||||
setConversationId(inbox.firestoreCollectionId)
|
||||
}}
|
||||
key={inbox.partner.id}
|
||||
h='60px'
|
||||
p={2}
|
||||
align='center'
|
||||
cursor='pointer'
|
||||
borderRadius='8px'
|
||||
mb={index != data.inboxes.length - 1 ? 2 : 0}
|
||||
bgColor={inbox.firestoreCollectionId == conversationId ? 'gray.100' : 'white'}
|
||||
_hover={{ bgColor: 'gray.200' }}
|
||||
onMouseOver={() => { setShowEllipsis(true) }}
|
||||
onMouseLeave={() => { setShowEllipsis(false) }}
|
||||
>
|
||||
<Avatar w='46px' h='46px' src={inbox.partner.avatarUrl} />
|
||||
<Text ml={4} size='md'>u/{inbox.partner.username}</Text>
|
||||
{
|
||||
showEllipsis &&
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label=''
|
||||
ml='auto'
|
||||
borderRadius='50%'
|
||||
size='sm'
|
||||
bgColor='transparent'
|
||||
icon={<Box className='fa-solid fa-ellipsis' />}
|
||||
_focusVisible={{ boxShadow: '0 0 0 3px #48BB78' }}
|
||||
_hover={{ bgColor: 'gray.300' }}
|
||||
_active={{ bgColor: 'gray.400' }}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w='7.5rem'>
|
||||
<PopoverArrow />
|
||||
<PopoverBody pl={2} pr={2}>
|
||||
<VStack align='stretch'>
|
||||
<Button w='full' onClick={onOpen} color='red.600'>Remove</Button>
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Delete Conversation</ModalHeader>
|
||||
<ModalCloseButton _focusVisible={{ boxShadow: '0 0 0 3px #68D391;' }} />
|
||||
<ModalBody>
|
||||
Are you sure you want to delete this conversation?
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex justify='center' align='center' w='full'>
|
||||
<Button colorScheme='red' onClick={async () => {
|
||||
await deleteConversation({ firestoreCollectionId: conversationId })
|
||||
onClose()
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Box, Button, Flex, Heading, Image, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { DateTime } from 'luxon'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { OffersQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeleteOfferButtons } from './EditDeleteOfferButtons'
|
||||
|
||||
interface IndexOffersProps {
|
||||
offersData: OffersQuery
|
||||
offersFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
offersVariables: { limit: number, cursor: string | null }
|
||||
setOffersVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null }>>
|
||||
}
|
||||
|
||||
export const IndexOffers = (props: IndexOffersProps): JSX.Element => {
|
||||
const { offersData, offersFetching, user, offersVariables, setOffersVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!offersData && offersFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
offersData?.offers?.offers?.length > 0 &&
|
||||
offersData.offers.offers.map(o =>
|
||||
o &&
|
||||
<Flex key={o.id} pl={5} pr={5} pt={3} pb={3} minH='40' shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Image src={o.photoUrl} fallbackSrc='/samples/square.webp' alt='offer-photo' w='32' h='32' mr={4} />
|
||||
<Box h='full'>
|
||||
<NextLink passHref href='/offer/[id]' as={`/offer/${o.id}`} >
|
||||
<Link color='green.500' _hover={{ textDecor: 'underline', textDecorationColor: 'green.500' }}>
|
||||
<Heading fontSize='xl'>{o.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex align='center' fontSize='xs' mb={1} color='blackAlpha.600'>
|
||||
<Text>Posted by </Text>
|
||||
<NextLink passHref
|
||||
href={o.creatorType == 'user' ? `/u/[user]` : `/p/[page]`}
|
||||
as={o.creatorType == 'user' ? `/u/${o.creator.username}` : `/p/${o.pageCreator.pageName}`}
|
||||
>
|
||||
<Link>
|
||||
{o.creatorType == 'user' ? `u/${o.creator.username}` : `p/${o?.pageCreator.pageName}`}
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Text> in </Text>
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${o.space.spaceName}`}>
|
||||
<Link>
|
||||
s/{o.space.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.workplace}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.address}</Text>
|
||||
<Text fontSize='md' mb={1} color={o.recruiting ? 'green.500' : 'red.500'} fontWeight='bold'>{o.recruiting ? 'Recruiting' : 'Not recruiting'}</Text>
|
||||
<Text fontSize='sm' color='blackAlpha.600'>{DateTime.fromMillis(parseInt(o.createdAt)).toRelative()}</Text>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeleteOfferButtons id={o.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
offersData?.offers?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOffersVariables({
|
||||
limit: offersVariables.limit,
|
||||
cursor: offersData.offers.offers[offersData.offers.offers.length - 1].createdAt
|
||||
})
|
||||
}}
|
||||
isLoading={offersFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Box, Button, Flex, Heading, Link, Stack, Tag, TagLabel, TagLeftIcon, Text } from '@chakra-ui/react'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dynamic from 'next/dynamic'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import NoSSR from 'react-no-ssr'
|
||||
import { PostsQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeletePostButtons } from './EditDeletePostButtons'
|
||||
import { tagStylingMap } from './tagStylingMap'
|
||||
import { VoteSection } from './VoteSection'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface IndexPostsProps {
|
||||
postsData: PostsQuery
|
||||
postsFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
postsVariables: { limit: number, cursor: string | null }
|
||||
setPostsVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null }>>
|
||||
}
|
||||
|
||||
export const IndexPosts = (props: IndexPostsProps): JSX.Element => {
|
||||
const { postsData, postsFetching, user, postsVariables, setPostsVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!postsData || postsFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
postsData?.posts?.posts?.length > 0 &&
|
||||
postsData.posts.posts.map(p =>
|
||||
p &&
|
||||
<Flex key={p.id} pl={5} pr={5} pt={3} pb={3} shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<VoteSection post={cloneDeep(p)} /> {/* clone to avoid side effect of up/downvoting on video */}
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Box h='full'>
|
||||
<Flex fontSize='sm' mt={1.5} mb={1}>
|
||||
<Text>Posted by </Text>
|
||||
<NextLink passHref
|
||||
href={p.creatorType == 'user' ? `/u/[user]` : `/p/[page]`}
|
||||
as={p.creatorType == 'user' ? `/u/${p.creator.username}` : `/p/${p?.pageCreator.pageName}`}
|
||||
>
|
||||
<Link>
|
||||
{p.creatorType == 'user' ? `u/${p.creator.username}` : `p/${p?.pageCreator.pageName}`}
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Text> in </Text>
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${p?.space?.spaceName}`}>
|
||||
<Link>
|
||||
s/{p?.space?.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<NextLink passHref href='/post/[id]' as={`/post/${p.id}`} >
|
||||
<Link>
|
||||
<Heading fontSize='xl' mb={2}>{p.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex mb={3}>
|
||||
{
|
||||
p.tags?.map((t, index) => {
|
||||
const tagStyling = tagStylingMap[`${t.name}`]
|
||||
return (
|
||||
t &&
|
||||
<Tag key={index} colorScheme={tagStyling.colorScheme} mr={2}>
|
||||
<Flex align='center'>
|
||||
{
|
||||
tagStyling.icon &&
|
||||
<Flex w='20px' justify='center' align='center' mr={1}>
|
||||
<TagLeftIcon as={() => tagStyling.icon} />
|
||||
</Flex>
|
||||
}
|
||||
<TagLabel color={tagStyling.textColor ? tagStyling.textColor : undefined}>{t.name}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
<NoSSR onSSR='Loading...'>
|
||||
<QuillDisplay value={p.text} />
|
||||
</NoSSR>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == p?.creator?.id) || checkPageOwnership(p?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeletePostButtons id={p.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
postsData?.posts?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPostsVariables({
|
||||
limit: postsVariables.limit,
|
||||
cursor: postsData.posts.posts[postsData.posts.posts.length - 1].createdAt
|
||||
})
|
||||
}}
|
||||
isLoading={postsFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { FormControl, FormErrorMessage, FormLabel, Input, Textarea } from '@chakra-ui/react'
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import autosize from 'autosize'
|
||||
import { useField } from 'formik'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { InputHTMLAttributes, TextareaHTMLAttributes, useEffect, useRef } from 'react'
|
||||
const Quill = dynamic(() => import('./Quill.client'), { ssr: false })
|
||||
const ReactQuill = dynamic(() => import('react-quill'), { ssr: false })
|
||||
|
||||
type InputFieldProps =
|
||||
(
|
||||
InputHTMLAttributes<HTMLInputElement> & PropsOf<typeof Input> &
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement> & PropsOf<typeof Textarea> &
|
||||
PropsOf<typeof ReactQuill>
|
||||
) & {
|
||||
label: string
|
||||
name: string
|
||||
inputType?: 'input' | 'textarea' | 'quill'
|
||||
onQuillChangeEffect?: () => void
|
||||
}
|
||||
|
||||
type InputComponentProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label: string
|
||||
name: string
|
||||
size?: any
|
||||
}
|
||||
|
||||
type TextAreaComponentProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
label: string
|
||||
name: string
|
||||
size?: any
|
||||
}
|
||||
|
||||
type QuillComponentProps = PropsOf<typeof ReactQuill> & {
|
||||
label: string
|
||||
name: string
|
||||
onQuillChangeEffect?: () => void
|
||||
}
|
||||
|
||||
const InputComponent = (props: InputComponentProps): JSX.Element => {
|
||||
const { label, name, ...rest } = props
|
||||
const [field, { error }] = useField(props)
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<Input {...field} {...props} id={field.name} focusBorderColor='green.400' spellCheck={false} />
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
const TextAreaComponent = (props: TextAreaComponentProps): JSX.Element => {
|
||||
const { label, name, ...rest } = props
|
||||
const [field, { error }] = useField(props)
|
||||
const ref = useRef()
|
||||
useEffect(() => {
|
||||
const current = ref.current
|
||||
autosize(current)
|
||||
return () => {
|
||||
autosize.destroy(current)
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<Textarea {...field} {...props} id={field.name} ref={ref} focusBorderColor='green.400' spellCheck={false} />
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
const QuillComponent = (props: QuillComponentProps): JSX.Element => {
|
||||
const { label, name, onQuillChangeEffect, ...rest } = props
|
||||
const [{ name: fieldName, onBlur, value }, { error }, { setValue }] = useField<string>({ name })
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setValue(value)
|
||||
if (onQuillChangeEffect) {
|
||||
onQuillChangeEffect()
|
||||
}
|
||||
}
|
||||
const handleEditorBlur = () => {
|
||||
onBlur({ target: { name } })
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={fieldName}>{label}</FormLabel>
|
||||
<Quill
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlur={handleEditorBlur}
|
||||
id={fieldName}
|
||||
{...props}
|
||||
|
||||
/>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export const InputField = (props: InputFieldProps): JSX.Element => {
|
||||
const { label, name, inputType = 'input', onQuillChangeEffect, ...rest } = props
|
||||
return (
|
||||
<>
|
||||
{
|
||||
inputType == 'input' &&
|
||||
<InputComponent label={label} name={name} {...rest} />
|
||||
}
|
||||
{
|
||||
inputType == 'textarea' &&
|
||||
<TextAreaComponent label={label} name={name} {...rest} />
|
||||
}
|
||||
{
|
||||
inputType == 'quill' &&
|
||||
<QuillComponent label={label} name={name} {...rest} onQuillChangeEffect={onQuillChangeEffect} />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
|
||||
export const Interpunct = (): JSX.Element => {
|
||||
return (
|
||||
<Text> · </Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Box } from '@chakra-ui/react'
|
||||
import { NavBar } from './NavBar'
|
||||
import { Wrapper, WrapperVariant } from './Wrapper'
|
||||
|
||||
interface LayoutProps {
|
||||
children?: React.ReactNode
|
||||
variant?: WrapperVariant
|
||||
navBarShadow?: boolean
|
||||
}
|
||||
|
||||
export const Layout = (props: LayoutProps): JSX.Element => {
|
||||
const { children, variant, navBarShadow } = props
|
||||
return (
|
||||
<Box bgColor='#F3F2EF' minH='100vh' w='100vw' overflowX='hidden'>
|
||||
<NavBar shadow={navBarShadow} />
|
||||
<Wrapper variant={variant}>
|
||||
{children}
|
||||
</Wrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Image } from '@chakra-ui/react'
|
||||
import { ImageProps } from '@chakra-ui/react'
|
||||
|
||||
type LogoProps = ImageProps & {
|
||||
density?: string | number
|
||||
}
|
||||
|
||||
export const Logo = (props: LogoProps): JSX.Element => {
|
||||
const { density = 500 } = props
|
||||
return (
|
||||
<Image src={`/logos/logo.${density}.webp`} alt='Logo' sizes='fixed' {...props} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ButtonProps, IconButton } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { Logo } from './Logo'
|
||||
|
||||
export const LogoButton = (props: ButtonProps): JSX.Element => {
|
||||
const [logoDensity, setLogoDensity] = useState(500)
|
||||
return (
|
||||
<IconButton
|
||||
aria-label='app-icon'
|
||||
icon={<Logo density={logoDensity} w='full' h='full' />}
|
||||
bgColor='transparent'
|
||||
_hover={{ bgColor: 'transparent' }}
|
||||
_active={{ bgColor: 'transparent' }}
|
||||
onMouseOver={() => setLogoDensity(600)}
|
||||
onMouseLeave={() => setLogoDensity(500)}
|
||||
onMouseDown={() => setLogoDensity(700)}
|
||||
onMouseUp={() => setLogoDensity(500)}
|
||||
w='35px !important'
|
||||
h='35px !important'
|
||||
minW='0px'
|
||||
minH='0px'
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Avatar, Box, Button, Flex, IconButton, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, Text, VStack } from '@chakra-ui/react'
|
||||
import { DocumentData } from 'firebase/firestore'
|
||||
import { useState } from 'react'
|
||||
import { ParticipantsQuery, useDeleteMessageMutation } from 'src/generated/graphql'
|
||||
import { useUser } from 'src/hooks'
|
||||
|
||||
interface MessageProps {
|
||||
message: DocumentData
|
||||
userId: number
|
||||
user?: ReturnType<typeof useUser>
|
||||
partner?: ParticipantsQuery['participants'][0]
|
||||
conversationId: string
|
||||
}
|
||||
|
||||
export const Message = (props: MessageProps): JSX.Element => {
|
||||
const { message, userId, user, partner, conversationId } = props
|
||||
const [showEllipsis, setShowEllipsis] = useState(false)
|
||||
const [, deleteMessage] = useDeleteMessageMutation()
|
||||
return (
|
||||
<Flex onMouseOver={() => { setShowEllipsis(true) }} onMouseLeave={() => { setShowEllipsis(false) }} w='full' align='center'>
|
||||
{
|
||||
message.senderId != userId ? (
|
||||
<>
|
||||
<Avatar
|
||||
src={message.senderId == userId ? user?.avatarUrl : partner?.avatarUrl}
|
||||
w='30px'
|
||||
h='30px'
|
||||
/>
|
||||
<Text
|
||||
maxW='70%'
|
||||
m='0.5rem 0'
|
||||
borderRadius='0.5rem'
|
||||
fontSize='0.85rem'
|
||||
fontFamily='"Segoe UI Historic", "Segoe UI", Helvetica, Arial, sans-serif'
|
||||
p='0.5rem 0.75rem'
|
||||
resize='none'
|
||||
mr='auto'
|
||||
ml='0.75rem'
|
||||
color='#3A3B3C'
|
||||
bgColor='#E4E6EB'
|
||||
>
|
||||
{message.content}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
showEllipsis &&
|
||||
<Popover placement='top'>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label=''
|
||||
ml='auto'
|
||||
mr={2}
|
||||
borderRadius='50%'
|
||||
size='sm'
|
||||
bgColor='transparent'
|
||||
icon={<Box className='fa-solid fa-ellipsis' />}
|
||||
_focusVisible={{ boxShadow: '0 0 0 3px #48BB78' }}
|
||||
_hover={{ bgColor: 'gray.300' }}
|
||||
_active={{ bgColor: 'gray.400' }}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w='7.5rem'>
|
||||
<PopoverArrow />
|
||||
<PopoverBody pl={2} pr={2}>
|
||||
<VStack align='stretch'>
|
||||
<Button w='full' onClick={async () => {
|
||||
await deleteMessage({
|
||||
firestoreCollectionId: conversationId,
|
||||
messageDocumentId: message.id,
|
||||
})
|
||||
}} color='red.600'>Remove</Button>
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
}
|
||||
<Text
|
||||
maxW='70%'
|
||||
m='0.5rem 0'
|
||||
borderRadius='0.5rem'
|
||||
fontSize='0.85rem'
|
||||
fontFamily='"Segoe UI Historic", "Segoe UI", Helvetica, Arial, sans-serif'
|
||||
p='0.5rem 0.75rem'
|
||||
resize='none'
|
||||
mr='0.75rem'
|
||||
color='white'
|
||||
bgColor='gray.500'
|
||||
ml={!showEllipsis ? 'auto' : undefined}
|
||||
>
|
||||
{message.content}
|
||||
</Text>
|
||||
<Avatar
|
||||
src={message.senderId == userId ? user?.avatarUrl : partner?.avatarUrl}
|
||||
w='30px'
|
||||
h='30px'
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Avatar, Box, Button, Flex, Link, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, VStack } from '@chakra-ui/react'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useGetSignedUrlQuery, useLogoutMutation, useMeQuery } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { LogoButton } from './LogoButton'
|
||||
import { NoUnderlineLink } from './NoUnderlineLink'
|
||||
import { OutlinedButton } from './OutlinedButton'
|
||||
|
||||
interface NavBarProps {
|
||||
shadow?: boolean
|
||||
}
|
||||
|
||||
export const NavBar = (props: NavBarProps): JSX.Element => {
|
||||
const { shadow = true } = props
|
||||
const [{ data, fetching }] = useMeQuery()
|
||||
|
||||
const [{ data: avatarData }] = useGetSignedUrlQuery({
|
||||
pause: !data?.me?.avatar,
|
||||
variables: {
|
||||
key: data?.me?.avatar,
|
||||
}
|
||||
})
|
||||
const router = useRouter()
|
||||
const [{ fetching: logoutFetching }, logout] = useLogoutMutation()
|
||||
const [showCreatePost, setShowCreatePost] = useState(true)
|
||||
const [showCreateOffer, setShowCreateOffer] = useState(true)
|
||||
const windowRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (windowRef?.current?.clientWidth < 532) {
|
||||
setShowCreatePost(false)
|
||||
setShowCreateOffer(false)
|
||||
}
|
||||
else if (windowRef?.current?.clientWidth < 684) {
|
||||
setShowCreatePost(false)
|
||||
setShowCreateOffer(true)
|
||||
}
|
||||
else {
|
||||
setShowCreatePost(true)
|
||||
setShowCreateOffer(true)
|
||||
}
|
||||
}, [windowRef?.current?.clientWidth])
|
||||
|
||||
let userSection = null
|
||||
if (fetching) { } // data is loading
|
||||
else if (!data?.me) { // user not logged in
|
||||
userSection =
|
||||
<>
|
||||
<NextLink passHref href='/login'>
|
||||
<OutlinedButton as={NoUnderlineLink} w='6.5rem' mr={5}>
|
||||
Login
|
||||
</OutlinedButton>
|
||||
</NextLink>
|
||||
<NextLink passHref href='/sign-up'>
|
||||
<ContainedButton as={NoUnderlineLink} w='6.5rem'>
|
||||
Register
|
||||
</ContainedButton>
|
||||
</NextLink>
|
||||
</>
|
||||
}
|
||||
else { // user logged in
|
||||
userSection =
|
||||
<Flex alignItems='center'>
|
||||
{
|
||||
(!router?.asPath.includes('create-post') && showCreatePost) &&
|
||||
<NextLink passHref href='/create-post'>
|
||||
<ContainedButton as={NoUnderlineLink} mr={4} w='32'>
|
||||
Create Post
|
||||
</ContainedButton>
|
||||
</NextLink>
|
||||
}
|
||||
{
|
||||
(!router?.asPath.includes('create-offer') && showCreateOffer) &&
|
||||
<NextLink passHref href='/create-offer'>
|
||||
<OutlinedButton as={NoUnderlineLink} mr={6} w='32'>
|
||||
Create Offer
|
||||
</OutlinedButton>
|
||||
</NextLink>
|
||||
}
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Avatar
|
||||
name={data?.me?.username}
|
||||
src={avatarData?.getSignedUrl}
|
||||
w='40px'
|
||||
h='40px'
|
||||
mr={6}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent boxShadow='none !important'>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
<VStack align='stretch'>
|
||||
<NextLink passHref href='/u/[user]' as={`/u/${data.me.username}`}>
|
||||
<Button as={NoUnderlineLink} color='green.400'>Profile</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href='/manage-pages'>
|
||||
<Button as={NoUnderlineLink} color='green.400'>Manage Pages</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href='/manage-spaces'>
|
||||
<Button as={NoUnderlineLink} color='green.400'>Manage Spaces</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href='/chat'>
|
||||
<Button as={NoUnderlineLink} color='green.400'>Chat</Button>
|
||||
</NextLink>
|
||||
<NextLink passHref href='/dashboard'>
|
||||
<Button as={NoUnderlineLink} color='green.400'>Dashboard</Button>
|
||||
</NextLink>
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
router.reload()
|
||||
}}
|
||||
isLoading={logoutFetching}
|
||||
color='blackAlpha.600'
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
return (
|
||||
<Flex ref={windowRef} position='fixed' zIndex='1402' w='full' alignItems='center' h='50px' pl='20' pr='20' bgColor='white' shadow={!shadow ? 'none' : 'lg'} mb={4}>
|
||||
<NextLink passHref href='/'>
|
||||
<Link>
|
||||
<LogoButton />
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Box ml='auto'>{userSection}</Box>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Link, LinkProps } from '@chakra-ui/react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type NoUnderlineLinkProps = LinkProps & {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const NoUnderlineLink = (props: NoUnderlineLinkProps) => {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<Link
|
||||
textDecoration='none'
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
_active={{ textDecoration: 'none' }}
|
||||
_visited={{ textDecoration: 'none' }}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button, ButtonProps, ColorProps } from '@chakra-ui/react'
|
||||
import { DataType } from 'csstype'
|
||||
|
||||
type OutlinedButtonProps = ButtonProps & {
|
||||
color?: DataType.NamedColor
|
||||
baseColorLevel?: 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
|
||||
}
|
||||
|
||||
export const OutlinedButton = (props: OutlinedButtonProps): JSX.Element => {
|
||||
const { color = 'green', children, baseColorLevel = 500 } = props
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
bgColor='white'
|
||||
borderWidth='3px'
|
||||
borderColor={`${color}.${baseColorLevel}`}
|
||||
textColor={`${color}.${baseColorLevel}`}
|
||||
_hover={{ bgColor: `${color}.50` }}
|
||||
_active={{ bgColor: `${color}.100` }}
|
||||
_focusVisible={{ boxShadow: 'none' }}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Box, Heading } from '@chakra-ui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useMemo } from 'react'
|
||||
import { PageQuery } from 'src/generated/graphql'
|
||||
import { EditPageAboutButton } from './EditPageAboutButton'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface PageMainProps {
|
||||
data: PageQuery
|
||||
}
|
||||
|
||||
export const PageMain = (props: PageMainProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const pageId = useMemo(() => data?.page?.id, [data])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>About</Heading>
|
||||
{
|
||||
data?.page?.ownerStatus &&
|
||||
<EditPageAboutButton data={data} />
|
||||
}
|
||||
<QuillDisplay value={data?.page?.about} />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Box, Button, Flex, Heading, Image, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { DateTime } from 'luxon'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { OffersQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeleteOfferButtons } from './EditDeleteOfferButtons'
|
||||
|
||||
interface PageOffersProps {
|
||||
offersData: OffersQuery
|
||||
offersFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
offersVariables: { limit: number, cursor: string | null, pageId: number }
|
||||
setOffersVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, pageId: number }>>
|
||||
}
|
||||
|
||||
export const PageOffers = (props: PageOffersProps): JSX.Element => {
|
||||
const { offersData, offersFetching, user, offersVariables, setOffersVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!offersData && offersFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
offersData?.offers?.offers?.length > 0 &&
|
||||
offersData.offers.offers.map(o =>
|
||||
o &&
|
||||
<Flex key={o.id} pl={5} pr={5} pt={3} pb={3} minH='40' shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Image src={o.photoUrl} alt='offer-photo' fallbackSrc='/samples/square.webp' w='32' h='32' mr={4} />
|
||||
<Box h='full'>
|
||||
<NextLink passHref href='/offer/[id]' as={`/offer/${o.id}`} >
|
||||
<Link color='green.500' _hover={{ textDecor: 'underline', textDecorationColor: 'green.500' }}>
|
||||
<Heading fontSize='xl'>{o.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Text fontSize='xs' mb={1} color='blackAlpha.600'>Posted in
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${o.space.spaceName}`}>
|
||||
<Link>
|
||||
s/{o.space.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.workplace}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.address}</Text>
|
||||
<Text fontSize='md' mb={1} color={o.recruiting ? 'green.500' : 'red.500'} fontWeight='bold'>{o.recruiting ? 'Recruiting' : 'Not recruiting'}</Text>
|
||||
<Text fontSize='sm' color='blackAlpha.600'>{DateTime.fromMillis(parseInt(o.createdAt)).toRelative()}</Text>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == o.creator?.id) || checkPageOwnership(o.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeleteOfferButtons id={o.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
offersData?.offers?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOffersVariables({
|
||||
limit: offersVariables.limit,
|
||||
cursor: offersData.offers.offers[offersData.offers.offers.length - 1].createdAt,
|
||||
pageId: offersVariables.pageId
|
||||
})
|
||||
}}
|
||||
isLoading={offersFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Link, Box, Button, Flex, Heading, Stack, Tag, TagLabel, TagLeftIcon, Text } from '@chakra-ui/react'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dynamic from 'next/dynamic'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import NoSSR from 'react-no-ssr'
|
||||
import { PostsQuery } from 'src/generated/graphql'
|
||||
import { useUser, useCheckPageOwnership } from 'src/hooks'
|
||||
import { EditDeletePostButtons } from './EditDeletePostButtons'
|
||||
import { tagStylingMap } from './tagStylingMap'
|
||||
import { VoteSection } from './VoteSection'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface PagePostsProps {
|
||||
postsData: PostsQuery
|
||||
postsFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
postsVariables: { limit: number, cursor: string | null, pageId: number }
|
||||
setPostsVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, pageId: number }>>
|
||||
}
|
||||
|
||||
export const PagePosts = (props: PagePostsProps): JSX.Element => {
|
||||
const { postsData, postsFetching, user, postsVariables, setPostsVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!postsData && postsFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
postsData?.posts?.posts?.length > 0 &&
|
||||
postsData.posts.posts.map(p =>
|
||||
p &&
|
||||
<Flex key={p.id} pl={5} pr={5} pt={3} pb={3} shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<VoteSection post={cloneDeep(p)} />
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Box h='full'>
|
||||
<Flex fontSize='sm' mt={1.5} mb={1}>
|
||||
<Text>Posted</Text>
|
||||
<Text> in </Text>
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${p?.space?.spaceName}`}>
|
||||
<Link>
|
||||
s/{p?.space?.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<NextLink passHref href='/post/[id]' as={`/post/${p.id}`} >
|
||||
<Link>
|
||||
<Heading fontSize='xl' mb={2}>{p.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex mb={3}>
|
||||
{
|
||||
p.tags?.map((t, index) => {
|
||||
const tagStyling = tagStylingMap[`${t.name}`]
|
||||
return (
|
||||
t &&
|
||||
<Tag key={index} colorScheme={tagStyling.colorScheme} mr={2}>
|
||||
<Flex align='center'>
|
||||
{
|
||||
tagStyling.icon &&
|
||||
<Flex w='20px' justify='center' align='center' mr={1}>
|
||||
<TagLeftIcon as={() => tagStyling.icon} />
|
||||
</Flex>
|
||||
}
|
||||
<TagLabel color={tagStyling.textColor ? tagStyling.textColor : undefined}>{t.name}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
<NoSSR onSSR='Loading...'>
|
||||
<QuillDisplay value={p.text} />
|
||||
</NoSSR>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == p?.creator?.id) || checkPageOwnership(p?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeletePostButtons id={p.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
postsData?.posts?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPostsVariables({
|
||||
limit: postsVariables.limit,
|
||||
cursor: postsData.posts.posts[postsData.posts.posts.length - 1].createdAt,
|
||||
pageId: postsVariables.pageId
|
||||
})
|
||||
}}
|
||||
isLoading={postsFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Avatar, Box, Flex, Heading, Image, Text } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { GetSignedUrlQuery, PageQuery } from 'src/generated/graphql'
|
||||
import { ChangePageAvatarButton } from './ChangePageAvatarButton'
|
||||
import { ChangePageCoverPhotoButton } from './ChangePageCoverPhotoButton'
|
||||
import { FollowPageButton } from './FollowPageButton'
|
||||
import { UpdatePageInfoButton } from './UpdatePageInfoButton'
|
||||
|
||||
interface PageProfileProps {
|
||||
data: PageQuery
|
||||
pageCoverPhotoData: GetSignedUrlQuery
|
||||
pageAvatarData: GetSignedUrlQuery
|
||||
fetching: boolean
|
||||
setAvatarKey: Dispatch<SetStateAction<string>>
|
||||
setCoverPhotoKey: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
export const PageProfile = (props: PageProfileProps): JSX.Element => {
|
||||
const { data, pageCoverPhotoData, setCoverPhotoKey, pageAvatarData, setAvatarKey, fetching } = props
|
||||
return (
|
||||
<Box w='full' bgColor='white' shadow='lg' mb='4' position='relative'>
|
||||
<Image src={pageCoverPhotoData?.getSignedUrl} alt='page-cover-photo' fallbackSrc='/samples/coverPhoto.webp' w='full' h='9.5rem' />
|
||||
{
|
||||
data?.page?.ownerStatus &&
|
||||
<ChangePageCoverPhotoButton data={data} setCoverPhotoKey={setCoverPhotoKey} />
|
||||
}
|
||||
<MUIBox sx={{ div: { fontSize: '2.25rem' } }}>
|
||||
<Avatar
|
||||
name='p /'
|
||||
src={pageAvatarData?.getSignedUrl}
|
||||
bgColor={pageAvatarData?.getSignedUrl ? 'transparent' : 'green.400'}
|
||||
color='white'
|
||||
size='2xl'
|
||||
position='absolute'
|
||||
zIndex={1}
|
||||
top='5.25rem'
|
||||
left='3rem'
|
||||
mr={6}
|
||||
objectPosition='relative'
|
||||
/>
|
||||
</MUIBox>
|
||||
<Flex flexDir='column' position='relative' w='full' pt='74px' pl='3rem' pb='3rem' pr='3rem'>
|
||||
{
|
||||
data?.page?.ownerStatus &&
|
||||
<>
|
||||
<UpdatePageInfoButton data={data} />
|
||||
<ChangePageAvatarButton data={data} setAvatarKey={setAvatarKey} />
|
||||
</>
|
||||
}
|
||||
<FollowPageButton
|
||||
data={data}
|
||||
top={6}
|
||||
right={data?.page?.ownerStatus ? '280px' : '3rem'}
|
||||
/>
|
||||
<Heading size='lg'>
|
||||
{data?.page?.pageName ? data?.page?.fullPageName ? data.page.fullPageName : `p/${data.page.pageName}` : fetching ? 'Loading...' : 'Page not found.'}
|
||||
</Heading>
|
||||
{
|
||||
data?.page?.fullPageName &&
|
||||
<Text color='gray.600'>p/{data?.page?.pageName}</Text>
|
||||
}
|
||||
<Heading size='md' mt={2}>{data?.page?.headline}</Heading>
|
||||
<Text color='gray.600' >{data?.page?.address}</Text>
|
||||
<Flex mt={2}>
|
||||
<Flex>
|
||||
<Text color='gray.700'>{Number(data?.page?.followerNumber).toLocaleString()}</Text>
|
||||
<Text color='gray.600'> followers</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Flex, FormControl, FormErrorMessage, FormLabel, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text } from '@chakra-ui/react'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import { blobToFile } from 'src/utils'
|
||||
import { useField } from 'formik'
|
||||
|
||||
type PhotoFieldProps = {
|
||||
label: string
|
||||
name: string
|
||||
type: 'avatar' | 'coverPhoto' | 'square'
|
||||
}
|
||||
|
||||
export const PhotoField = (props: PhotoFieldProps): JSX.Element => {
|
||||
const { label, name, type } = props
|
||||
const editor = useRef<AvatarEditor>(null)
|
||||
const [editorOptions, setEditorOptions] = useState<PropsOf<typeof AvatarEditor>>(null)
|
||||
const [field, { error }, { setValue }] = useField({ name })
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setEditorOptions(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
{
|
||||
editorOptions ?
|
||||
<Flex flexDir='column' align='center' >
|
||||
<MUIBox sx={{ canvas: { borderRadius: type == 'avatar' ? '50%' : 0 } }}>
|
||||
<AvatarEditor
|
||||
ref={editor}
|
||||
width={type == 'coverPhoto' ? 800 : 250}
|
||||
height={type == 'coverPhoto' ? 152 : 250}
|
||||
borderRadius={type == 'avatar' ? Infinity : 0}
|
||||
onPositionChange={(position) => { setEditorOptions({ ...editorOptions, position }) }}
|
||||
onImageChange={async () => {
|
||||
const dataURL = editor.current.getImageScaledToCanvas().toDataURL()
|
||||
const result = await fetch(dataURL)
|
||||
const blob = await result.blob()
|
||||
const file = blobToFile(blob, (editorOptions.image as File).name)
|
||||
setValue(file)
|
||||
}}
|
||||
{...editorOptions}
|
||||
/>
|
||||
</MUIBox>
|
||||
<Text mt={6}>You can select the part of the image to be included with your mouse.</Text>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Zoom</Text>
|
||||
<Slider
|
||||
aria-label='size-slider'
|
||||
defaultValue={1}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, scale: value }) }}
|
||||
min={1}
|
||||
max={2}
|
||||
step={0.01}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
<Flex align='center' w='80%' mt={2}>
|
||||
<Text w='50px' mr={4}>Rotate</Text>
|
||||
<Slider
|
||||
aria-label='rotate-slider'
|
||||
defaultValue={0}
|
||||
w='80%'
|
||||
size='lg'
|
||||
colorScheme='green'
|
||||
onChange={(value) => { setEditorOptions({ ...editorOptions, rotate: value }) }}
|
||||
min={0}
|
||||
max={180}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb borderWidth='3px' borderColor='green.500' />
|
||||
</Slider>
|
||||
</Flex>
|
||||
</Flex>
|
||||
:
|
||||
<MUIBox sx={{
|
||||
'& .dropzone': {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
border: '3px dashed #EEEEEE',
|
||||
backgroundColor: '#FAFAFA',
|
||||
color: '#BDBDBD',
|
||||
}
|
||||
}}>
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
onDrop={(acceptedFiles) => {
|
||||
setEditorOptions({ image: acceptedFiles[0] })
|
||||
setValue(acceptedFiles[0])
|
||||
}}
|
||||
>
|
||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||
<div {...getRootProps({ className: 'dropzone' })}>
|
||||
<input {...getInputProps()} />
|
||||
<Text>
|
||||
{
|
||||
isDragActive ?
|
||||
'Drop your image here ...' :
|
||||
'Drag \'n\' drop an image here, or click to select one'
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</MUIBox>
|
||||
}
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Box, Button, Flex, Heading, Image, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { DateTime } from 'luxon'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { OffersQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeleteOfferButtons } from './EditDeleteOfferButtons'
|
||||
|
||||
interface PostedOffersProps {
|
||||
me: ReturnType<typeof useUser>
|
||||
offersData: OffersQuery
|
||||
offersFetching: boolean
|
||||
offersVariables: { limit: number, cursor: string | null, userId: number }
|
||||
setOffersVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, userId: number }>>
|
||||
}
|
||||
|
||||
export const PostedOffers = (props: PostedOffersProps): JSX.Element => {
|
||||
const { me, offersData, offersFetching, offersVariables, setOffersVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!offersData && offersFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
offersData.offers.offers.length > 0 &&
|
||||
offersData.offers.offers.map(o =>
|
||||
o &&
|
||||
<Flex key={o.id} pl={5} pr={5} pt={3} pb={3} minH='40' shadow='md' borderWidth='1px' bgColor='white'>
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Image src={o.photoUrl} alt='offer-photo' fallbackSrc='/samples/square.webp' w='32' h='32' mr={4} />
|
||||
<Box h='full'>
|
||||
<NextLink passHref href='/offer/[id]' as={`/offer/${o.id}`} >
|
||||
<Link color='green.500' _hover={{ textDecor: 'underline', textDecorationColor: 'green.500' }}>
|
||||
<Heading fontSize='xl'>{o.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Text fontSize='xs' mb={1} color='blackAlpha.600'>Posted in
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${o.space.spaceName}`}>
|
||||
<Link>
|
||||
s/{o.space.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.workplace}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.address}</Text>
|
||||
<Text fontSize='md' mb={1} color={o.recruiting ? 'green.500' : 'red.500'} fontWeight='bold'>{o.recruiting ? 'Recruiting' : 'Not recruiting'}</Text>
|
||||
<Text fontSize='sm' color='blackAlpha.600'>{DateTime.fromMillis(parseInt(o.createdAt)).toRelative()}</Text>
|
||||
</Box>
|
||||
{
|
||||
((me?.id && me.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeleteOfferButtons id={o.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
offersData?.offers?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOffersVariables({
|
||||
limit: offersVariables.limit,
|
||||
cursor: offersData.offers.offers[offersData.offers.offers.length - 1].createdAt,
|
||||
userId: offersVariables.userId
|
||||
})
|
||||
}}
|
||||
isLoading={offersFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Avatar, Box, Flex, Heading, Image, Text } from '@chakra-ui/react'
|
||||
import { ChangeAvatarButton } from './ChangeAvatarButton'
|
||||
import { ChangeCoverPhotoButton } from './ChangeCoverPhotoButton'
|
||||
import { FollowUserButton } from './FollowUserButton'
|
||||
import { Interpunct } from './Interpunct'
|
||||
import { UpdateInfoButton } from './UpdateInfoButton'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { MeQuery, useGetSignedUrlQuery, UserQuery } from 'src/generated/graphql'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ProfileProps {
|
||||
data: UserQuery
|
||||
fetching: boolean
|
||||
me: MeQuery['me']
|
||||
}
|
||||
|
||||
export const Profile = (props: ProfileProps): JSX.Element => {
|
||||
const { data, fetching, me } = props
|
||||
const [avatarKey, setAvatarKey] = useState(data?.user?.avatar)
|
||||
const [coverPhotoKey, setCoverPhotoKey] = useState(data?.user?.coverPhoto)
|
||||
useEffect(() => {
|
||||
setAvatarKey(data?.user?.avatar)
|
||||
setCoverPhotoKey(data?.user?.coverPhoto)
|
||||
}, [data])
|
||||
const [{ data: avatarData }] = useGetSignedUrlQuery({
|
||||
pause: !avatarKey,
|
||||
variables: {
|
||||
key: avatarKey
|
||||
}
|
||||
})
|
||||
const [{ data: coverPhotoData }] = useGetSignedUrlQuery({
|
||||
pause: !coverPhotoKey,
|
||||
variables: {
|
||||
key: coverPhotoKey
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Box w='full' bgColor='white' shadow='lg' mb='4' position='relative'>
|
||||
<Image src={coverPhotoData?.getSignedUrl} alt='profile-cover-photo' fallbackSrc='/samples/coverPhoto.webp' w='full' h='9.5rem' />
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<ChangeCoverPhotoButton data={data} setCoverPhotoKey={setCoverPhotoKey} />
|
||||
}
|
||||
<MUIBox sx={{ div: { fontSize: '2.25rem' } }}>
|
||||
<Avatar
|
||||
name='u /'
|
||||
src={avatarData?.getSignedUrl}
|
||||
bgColor={avatarData?.getSignedUrl ? 'transparent' : 'green.400'}
|
||||
color='white'
|
||||
size='2xl'
|
||||
position='absolute'
|
||||
zIndex={1}
|
||||
top='5.25rem'
|
||||
left='3rem'
|
||||
mr={6}
|
||||
objectPosition='relative'
|
||||
/>
|
||||
</MUIBox>
|
||||
<Flex flexDir='column' position='relative' w='full' pt='74px' pl='3rem' pb='3rem' pr='3rem'>
|
||||
{
|
||||
data?.user?.id == me?.id ?
|
||||
<>
|
||||
<UpdateInfoButton data={data} />
|
||||
<ChangeAvatarButton data={data} setAvatarKey={setAvatarKey} />
|
||||
</>
|
||||
:
|
||||
<FollowUserButton data={data} />
|
||||
}
|
||||
<Heading size='lg'>
|
||||
{data?.user?.username ? data?.user?.fullName ? data.user.fullName : `u/${data.user.username}` : fetching ? 'Loading...' : 'User not found.'}
|
||||
</Heading>
|
||||
{
|
||||
data?.user?.fullName &&
|
||||
<Text color='gray.600'>u/{data?.user?.username}</Text>
|
||||
}
|
||||
<Heading size='md' mt={2}>{data?.user?.headline}</Heading>
|
||||
<Text color='gray.600' >{data?.user?.address}</Text>
|
||||
<Flex mt={2}>
|
||||
<Flex>
|
||||
<Text color='gray.700'>{Number(data?.user?.followerNumber).toLocaleString()}</Text>
|
||||
<Text color='gray.600'> followers</Text>
|
||||
</Flex>
|
||||
<Interpunct />
|
||||
<Flex>
|
||||
<Text color='gray.700'>{Number(data?.user?.followingNumber).toLocaleString()}</Text>
|
||||
<Text color='gray.600'> following</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Box, Button, Flex, GridItem, Heading, Icon, Image, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { DateTime } from 'luxon'
|
||||
import { MeQuery, QualificationsQuery, useDeleteQualificationMutation, useUpdateQualificationMutation, useUserQuery } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { DatePickerField } from './DatePickerField'
|
||||
import { InputField } from './InputField'
|
||||
import { Interpunct } from './Interpunct'
|
||||
import { PhotoField } from './PhotoField'
|
||||
import { SwitchField } from './SwitchField'
|
||||
|
||||
interface QualificationProps {
|
||||
data: ReturnType<typeof useUserQuery>[0]['data']
|
||||
me: MeQuery['me']
|
||||
item: QualificationsQuery['qualifications'][0]
|
||||
}
|
||||
|
||||
export const Qualification = (props: QualificationProps): JSX.Element => {
|
||||
const { data, me, item } = props
|
||||
const { isOpen: isEditingQualificationOpen, onOpen: onEditingQualificationOpen, onClose: onEditingQualificationClose } = useDisclosure()
|
||||
const [, updateQualification] = useUpdateQualificationMutation()
|
||||
const [, deleteQualification] = useDeleteQualificationMutation()
|
||||
|
||||
return (
|
||||
<GridItem key={item.id}>
|
||||
<Flex align='center' minH='62px'>
|
||||
<Image src={item.photoUrl} alt='qualification-photo' fallbackSrc='/samples/square.webp' w='45px' h='45px' mr={4} />
|
||||
<Box>
|
||||
<Heading size='sm'>{item.name}</Heading>
|
||||
<Text color='gray.600' fontSize='sm'>{item.issuingOrganisation}</Text>
|
||||
<Flex color='gray.600' fontSize='smaller'>
|
||||
{
|
||||
item.issuanceDate &&
|
||||
<>Issued {DateTime.fromISO(item.issuanceDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)}</>
|
||||
}
|
||||
{
|
||||
(item.issuanceDate && item.expirationDate) &&
|
||||
<Interpunct />
|
||||
}
|
||||
{
|
||||
item.expirationDate ?
|
||||
<> Expires on {DateTime.fromISO(item.issuanceDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)}</>
|
||||
:
|
||||
<> No Expiration Date</>
|
||||
}
|
||||
</Flex>
|
||||
<Text color='gray.600' fontSize='smaller'>Credential ID {item?.credentialID}</Text>
|
||||
{
|
||||
item.credentialURL &&
|
||||
<Link href={item.credentialURL} color='gray.700' fontSize='sm' mt={1}>See credential</Link>
|
||||
}
|
||||
</Box>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<Box ml='auto'>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={onEditingQualificationOpen} >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingQualificationOpen} onClose={onEditingQualificationClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ name: item.name, issuingOrganisation: item.issuingOrganisation, issuanceDate: item.issuanceDate, expire: item.expire, expirationDate: item.expirationDate, credentialID: item.credentialID, credentialURL: item.credentialURL, photo: null }}
|
||||
onSubmit={async values => {
|
||||
await updateQualification({ id: item.id, input: values })
|
||||
onEditingQualificationClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update license or certification</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='name' placeholder='' label='Name' />
|
||||
<Box mt={4}>
|
||||
<InputField name='issuingOrganisation' placeholder='' label='Issuing Organisation' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Issuance Date' name='issuanceDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<SwitchField label='Expire' name='expire' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<DatePickerField label='Expiration Date' name='expirationDate' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='credentialID' placeholder='' label='Credential ID' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='credentialURL' placeholder='' label='Credential URL' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<PhotoField label='Photo' name='photo' type='square' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
<Button variant='ghost' w='40px' h='40px' mr={2} onClick={async () => { await deleteQualification({ id: item.id }) }}>
|
||||
<Icon as={() => <Box className='fa-regular fa-trash' />} />
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</Flex>
|
||||
</GridItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import { Box } from '@mui/material'
|
||||
import ReactQuill from 'react-quill'
|
||||
import BlotFormatter from 'quill-blot-formatter'
|
||||
ReactQuill.Quill.register('modules/blotFormatter', BlotFormatter)
|
||||
|
||||
|
||||
const defaultToolbarOptions = [
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
['bold', 'italic', 'underline', 'link'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
[{ align: [] }],
|
||||
['image', 'video', 'blockquote'],
|
||||
]
|
||||
|
||||
const Quill = (props: PropsOf<typeof ReactQuill>): JSX.Element => {
|
||||
return (
|
||||
<Box className='quill-container' sx={{
|
||||
'& .quill': {
|
||||
'& .ql-toolbar': {
|
||||
borderRadius: '5px 5px 0 0'
|
||||
},
|
||||
'& .ql-container': {
|
||||
borderRadius: '0 0 5px 5px',
|
||||
'& .ql-editor': {
|
||||
minHeight: '10rem',
|
||||
},
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<ReactQuill
|
||||
className='quill'
|
||||
modules={{ toolbar: defaultToolbarOptions, blotFormatter: {} }}
|
||||
bounds={'.quill-container'}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Quill
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Box } from '@mui/material'
|
||||
import ReactQuill from 'react-quill'
|
||||
|
||||
interface QuillDisplayProps {
|
||||
value: string
|
||||
}
|
||||
|
||||
const QuillDisplay = (props: QuillDisplayProps): JSX.Element => {
|
||||
const { value } = props
|
||||
return (
|
||||
<Box className='quill-container' sx={{
|
||||
'& .quill': {
|
||||
'& .ql-toolbar': {
|
||||
display: 'none'
|
||||
},
|
||||
'& .ql-container': {
|
||||
border: 'none',
|
||||
'& .ql-editor': {
|
||||
padding: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<ReactQuill
|
||||
className='quill'
|
||||
readOnly={true}
|
||||
value={value}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuillDisplay
|
||||
@@ -0,0 +1,44 @@
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react'
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import { Select } from 'chakra-react-select'
|
||||
import { useField } from 'formik'
|
||||
import { SelectOption } from 'src/types'
|
||||
|
||||
type SelectFieldProps = PropsOf<typeof Select> & {
|
||||
label: string
|
||||
name: string
|
||||
isMulti: boolean
|
||||
defaultValue?: SelectOption | SelectOption[]
|
||||
options?: SelectOption[]
|
||||
}
|
||||
|
||||
export const SelectField = (props: SelectFieldProps): JSX.Element => {
|
||||
const { label, name, isMulti, defaultValue, options, ...rest } = props
|
||||
const [field, { error }, { setValue }] = useField({ name })
|
||||
|
||||
const handleChange = (selected: SelectOption | SelectOption[]) => {
|
||||
if (isMulti) {
|
||||
setValue((selected as SelectOption[]).map(t => t.value))
|
||||
}
|
||||
else {
|
||||
setValue((selected as SelectOption).value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<Select
|
||||
isMulti={isMulti}
|
||||
defaultValue={defaultValue}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
selectedOptionColor='green'
|
||||
focusBorderColor='green.500'
|
||||
useBasicStyles
|
||||
{...props}
|
||||
/>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Button, Icon, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Flex, useDisclosure, Box } from '@chakra-ui/react'
|
||||
import { Formik, Form } from 'formik'
|
||||
import { UserQuery, useSetSkillsMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { CreatableSelectField } from './CreatableSelectField'
|
||||
|
||||
interface SetSkillsButtonProps {
|
||||
data: UserQuery
|
||||
}
|
||||
|
||||
export const SetSkillsButton = (props: SetSkillsButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const { isOpen: isEditingSkillsOpen, onOpen: onEditingSkillsOpen, onClose: onEditingSkillsClose } = useDisclosure()
|
||||
const [, setSkills] = useSetSkillsMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onEditingSkillsOpen} variant='ghost' w='40px' h='40px' position='absolute' top={10} right='3rem' >
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' fontSize='1rem' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingSkillsOpen} onClose={onEditingSkillsClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ skills: data?.user?.skills }}
|
||||
onSubmit={async (values) => {
|
||||
setSkills(values)
|
||||
onEditingSkillsClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Set skills</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<CreatableSelectField
|
||||
label=''
|
||||
name='skills'
|
||||
defaultValue={data?.user?.skills?.length > 0 ? data.user.skills.map(skill => ({ value: skill, label: skill })) : []}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
import { Box, Heading } from '@chakra-ui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { SpaceQuery } from 'src/generated/graphql'
|
||||
import { EditSpaceAboutButton } from './EditSpaceAboutButton'
|
||||
import { EditSpaceRulesButton } from './EditSpaceRulesButton'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface SpaceMainProps {
|
||||
data: SpaceQuery
|
||||
}
|
||||
|
||||
export const SpaceMain = (props: SpaceMainProps): JSX.Element => {
|
||||
const { data } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>Rules</Heading>
|
||||
{
|
||||
data?.space?.modStatus &&
|
||||
<EditSpaceRulesButton data={data} />
|
||||
}
|
||||
<QuillDisplay value={data?.space?.rules} />
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>About</Heading>
|
||||
{
|
||||
data?.space?.modStatus &&
|
||||
<EditSpaceAboutButton data={data} />
|
||||
}
|
||||
<QuillDisplay value={data?.space?.about} />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Box, Button, Flex, Heading, Image, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { DateTime } from 'luxon'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { OffersQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeleteOfferButtons } from './EditDeleteOfferButtons'
|
||||
|
||||
interface SpaceOffersProps {
|
||||
offersData: OffersQuery
|
||||
offersFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
offersVariables: { limit: number, cursor: string | null, spaceId: number }
|
||||
setOffersVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, spaceId: number }>>
|
||||
}
|
||||
|
||||
export const SpaceOffers = (props: SpaceOffersProps): JSX.Element => {
|
||||
const { offersData, offersFetching, user, offersVariables, setOffersVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!offersData && offersFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
offersData?.offers?.offers?.length > 0 &&
|
||||
offersData.offers.offers.map(o =>
|
||||
o &&
|
||||
<Flex key={o.id} pl={5} pr={5} pt={3} pb={3} minH='40' shadow='md' borderWidth='1px' bgColor='white'>
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Image src={o.photoUrl} alt='offer-photo' fallbackSrc='/samples/square.webp' w='32' h='32' mr={4} />
|
||||
<Box h='full'>
|
||||
<NextLink passHref href='/offer/[id]' as={`/offer/${o.id}`} >
|
||||
<Link color='green.500' _hover={{ textDecor: 'underline', textDecorationColor: 'green.500' }}>
|
||||
<Heading fontSize='xl'>{o.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex align='center' fontSize='xs' mb={1} color='blackAlpha.600'>
|
||||
<Text>Posted by </Text>
|
||||
<NextLink passHref
|
||||
href={o.creatorType == 'user' ? `/u/[user]` : `/p/[page]`}
|
||||
as={o.creatorType == 'user' ? `/u/${o.creator.username}` : `/p/${o.pageCreator.pageName}`}
|
||||
>
|
||||
<Link>
|
||||
{o.creatorType == 'user' ? `u/${o.creator.username}` : `p/${o?.pageCreator.pageName}`}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.workplace}</Text>
|
||||
<Text fontSize='sm' mb={1} color='blackAlpha.600'>{o.address}</Text>
|
||||
<Text fontSize='md' mb={1} color={o.recruiting ? 'green.500' : 'red.500'} fontWeight='bold'>{o.recruiting ? 'Recruiting' : 'Not recruiting'}</Text>
|
||||
<Text fontSize='sm' color='blackAlpha.600'>{DateTime.fromMillis(parseInt(o.createdAt)).toRelative()}</Text>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeleteOfferButtons id={o.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
offersData?.offers?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOffersVariables({
|
||||
limit: offersVariables.limit,
|
||||
cursor: offersData.offers.offers[offersData.offers.offers.length - 1].createdAt,
|
||||
spaceId: offersVariables.spaceId
|
||||
})
|
||||
}}
|
||||
isLoading={offersFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Box, Button, Flex, Heading, Link, Stack, Tag, TagLabel, TagLeftIcon, Text } from '@chakra-ui/react'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dynamic from 'next/dynamic'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import NoSSR from 'react-no-ssr'
|
||||
import { PostsQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeletePostButtons } from './EditDeletePostButtons'
|
||||
import { tagStylingMap } from './tagStylingMap'
|
||||
import { VoteSection } from './VoteSection'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface SpacePostsProps {
|
||||
postsData: PostsQuery
|
||||
postsFetching: boolean
|
||||
user: ReturnType<typeof useUser>
|
||||
postsVariables: { limit: number, cursor: string | null, spaceId: number }
|
||||
setPostsVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, spaceId: number }>>
|
||||
}
|
||||
|
||||
export const SpacePosts = (props: SpacePostsProps): JSX.Element => {
|
||||
const { postsData, postsFetching, user, postsVariables, setPostsVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!postsData && postsFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
postsData?.posts?.posts?.length > 0 &&
|
||||
postsData.posts.posts.map(p =>
|
||||
p &&
|
||||
<Flex key={p.id} pl={5} pr={5} pt={3} pb={3} shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<VoteSection post={cloneDeep(p)} />
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Box h='full'>
|
||||
<Flex fontSize='sm' mt={1.5} mb={1}>
|
||||
<Text>Posted by </Text>
|
||||
<NextLink passHref
|
||||
href={p.creatorType == 'user' ? `/u/[user]` : `/p/[page]`}
|
||||
as={p.creatorType == 'user' ? `/u/${p.creator.username}` : `/p/${p?.pageCreator.pageName}`}
|
||||
>
|
||||
<Link>
|
||||
{p.creatorType == 'user' ? `u/${p.creator.username}` : `p/${p?.pageCreator.pageName}`}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<NextLink passHref href='/post/[id]' as={`/post/${p.id}`} >
|
||||
<Link>
|
||||
<Heading fontSize='xl' mb={2}>{p.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex mb={3}>
|
||||
{
|
||||
p.tags?.map((t, index) => {
|
||||
const tagStyling = tagStylingMap[`${t.name}`]
|
||||
return (
|
||||
<Tag key={index} colorScheme={tagStyling.colorScheme} mr={2}>
|
||||
<Flex align='center'>
|
||||
{
|
||||
tagStyling.icon &&
|
||||
<Flex w='20px' justify='center' align='center' mr={1}>
|
||||
<TagLeftIcon as={() => tagStyling.icon} />
|
||||
</Flex>
|
||||
}
|
||||
<TagLabel color={tagStyling.textColor ? tagStyling.textColor : undefined}>{t.name}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
<NoSSR onSSR='Loading...'>
|
||||
<QuillDisplay value={p.text} />
|
||||
</NoSSR>
|
||||
</Box>
|
||||
{
|
||||
((user?.id && user.id == p?.creator?.id) || checkPageOwnership(p?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeletePostButtons id={p.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
postsData?.posts?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPostsVariables({
|
||||
limit: postsVariables.limit,
|
||||
cursor: postsData.posts.posts[postsData.posts.posts.length - 1].createdAt,
|
||||
spaceId: postsVariables.spaceId
|
||||
})
|
||||
}}
|
||||
isLoading={postsFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Box, Text, Avatar, Flex, Heading, Image } from '@chakra-ui/react'
|
||||
import { Box as MUIBox } from '@mui/material'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { SpaceQuery, GetSignedUrlQuery } from 'src/generated/graphql'
|
||||
import { ChangeSpaceAvatarButton } from './ChangeSpaceAvatarButton'
|
||||
import { ChangeSpaceCoverPhotoButton } from './ChangeSpaceCoverPhotoButton'
|
||||
import { SubscribeSpaceButton } from './SubscribeSpaceButton'
|
||||
import { UpdateSpaceInfoButton } from './UpdateSpaceInfoButton'
|
||||
|
||||
interface SpaceProfileProps {
|
||||
data: SpaceQuery
|
||||
fetching: boolean
|
||||
spaceCoverPhotoData: GetSignedUrlQuery
|
||||
spaceAvatarData: GetSignedUrlQuery
|
||||
setAvatarKey: Dispatch<SetStateAction<string>>
|
||||
setCoverPhotoKey: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
export const SpaceProfile = (props: SpaceProfileProps): JSX.Element => {
|
||||
const { data, fetching, spaceCoverPhotoData, spaceAvatarData, setAvatarKey, setCoverPhotoKey } = props
|
||||
return (
|
||||
<Box w='full' bgColor='white' shadow='lg' mb='4' position='relative'>
|
||||
<Image src={spaceCoverPhotoData?.getSignedUrl} alt='space-cover-photo' fallbackSrc='/samples/coverPhoto.webp' w='full' h='9.5rem' />
|
||||
{
|
||||
data?.space?.modStatus &&
|
||||
<ChangeSpaceCoverPhotoButton data={data} setCoverPhotoKey={setCoverPhotoKey} />
|
||||
}
|
||||
<MUIBox sx={{ div: { fontSize: '2.25rem' } }}>
|
||||
<Avatar
|
||||
name='s /'
|
||||
src={spaceAvatarData?.getSignedUrl}
|
||||
bgColor={spaceAvatarData?.getSignedUrl ? 'transparent' : 'green.400'}
|
||||
color='white'
|
||||
size='2xl'
|
||||
position='absolute'
|
||||
zIndex={1}
|
||||
top='5.25rem'
|
||||
left='3rem'
|
||||
mr={6}
|
||||
objectPosition='relative'
|
||||
/>
|
||||
</MUIBox>
|
||||
<Flex flexDir='column' position='relative' w='full' pt='74px' pl='3rem' pb='3rem' pr='3rem'>
|
||||
{
|
||||
data?.space?.modStatus &&
|
||||
<>
|
||||
<UpdateSpaceInfoButton data={data} />
|
||||
<ChangeSpaceAvatarButton data={data} setAvatarKey={setAvatarKey} />
|
||||
</>
|
||||
}
|
||||
<SubscribeSpaceButton
|
||||
data={data}
|
||||
top={6}
|
||||
right={data?.space?.modStatus ? '280px' : '3rem'}
|
||||
/>
|
||||
<Heading size='lg'>
|
||||
{data?.space?.spaceName ? data?.space?.fullSpaceName ? data.space.fullSpaceName : `s/${data.space.spaceName}` : fetching ? 'Loading...' : 'Space not found.'}
|
||||
</Heading>
|
||||
{
|
||||
data?.space?.fullSpaceName &&
|
||||
<Text color='gray.600'>s/{data?.space?.spaceName}</Text>
|
||||
}
|
||||
<Heading size='md' mt={2}>{data?.space?.headline}</Heading>
|
||||
<Flex mt={2}>
|
||||
<Flex>
|
||||
<Text color='gray.700'>{Number(data?.space?.subscriberNumber).toLocaleString()}</Text>
|
||||
<Text color='gray.600'> subscribers</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react'
|
||||
import { SpaceQuery, useSubscribeMutation, useSubscriptionStatusQuery, useUnsubscribeMutation } from 'src/generated/graphql'
|
||||
import { useRequireLogin } from 'src/hooks'
|
||||
|
||||
type SubscribeSpaceButtonProps = ButtonProps & {
|
||||
data: SpaceQuery
|
||||
}
|
||||
|
||||
export const SubscribeSpaceButton = (props: SubscribeSpaceButtonProps): JSX.Element => {
|
||||
const { data, ...rest } = props
|
||||
const requireLogin = useRequireLogin()
|
||||
const [, subscribe] = useSubscribeMutation()
|
||||
const [, unsubscribe] = useUnsubscribeMutation()
|
||||
const [{ data: spaceSubscriptionStatusData }] = useSubscriptionStatusQuery({
|
||||
pause: !data?.space?.id,
|
||||
variables: {
|
||||
spaceId: data?.space?.id
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
position='absolute'
|
||||
color={spaceSubscriptionStatusData?.subscriptionStatus ? 'red.500' : 'green.500'}
|
||||
onClick={async () => {
|
||||
requireLogin()
|
||||
if (spaceSubscriptionStatusData?.subscriptionStatus) {
|
||||
await unsubscribe({ spaceId: data?.space?.id })
|
||||
}
|
||||
else {
|
||||
await subscribe({ spaceId: data?.space?.id })
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
spaceSubscriptionStatusData?.subscriptionStatus ? 'unsubscribe' : 'subscribe'
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FormControl, FormErrorMessage, FormLabel, Switch } from '@chakra-ui/react'
|
||||
import { SwitchProps } from '@mui/material'
|
||||
import { useField } from 'formik'
|
||||
import { ChangeEvent } from 'react'
|
||||
|
||||
type SwitchFieldProps = SwitchProps & any & {
|
||||
label: string
|
||||
name: string
|
||||
onSwitchChangeEffect?: () => void
|
||||
}
|
||||
|
||||
export const SwitchField = (props: SwitchFieldProps): JSX.Element => {
|
||||
const { label, name, onSwitchChangeEffect, ...rest } = props
|
||||
const [field, { error }, { setValue }] = useField({ name })
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.checked)
|
||||
if (onSwitchChangeEffect) {
|
||||
onSwitchChangeEffect()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={!!error}>
|
||||
<FormLabel htmlFor={field.name}>{label}</FormLabel>
|
||||
<Switch
|
||||
isChecked={field.value}
|
||||
onChange={handleChange}
|
||||
id={field.name}
|
||||
colorScheme='green'
|
||||
{...props}
|
||||
/>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Button, Grid, GridItem } from '@chakra-ui/react'
|
||||
import capitalize from 'lodash/capitalize'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface TabButtonsProps {
|
||||
tabs: string[]
|
||||
defaultTab: string
|
||||
setActive: Function
|
||||
}
|
||||
|
||||
export const TabButtons = (props: TabButtonsProps): JSX.Element => {
|
||||
const { tabs, defaultTab, setActive } = props
|
||||
const [activeTab, setActiveTab] = useState(defaultTab)
|
||||
return (
|
||||
<Grid gridTemplateColumns={`repeat(${tabs.length}, 6.5rem)`} columnGap='2rem' position='sticky' top='0'>
|
||||
{
|
||||
tabs.length > 0 &&
|
||||
tabs.map((tab, index) =>
|
||||
<GridItem key={index}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveTab(tab)
|
||||
setActive(tab)
|
||||
}}
|
||||
w='full'
|
||||
minW='6.5rem'
|
||||
bgColor={activeTab == tab ? 'white' : 'transparent'}
|
||||
textColor={activeTab == tab ? 'blackAlpha.600' : 'green.400'}
|
||||
fontWeight='semibold'
|
||||
>
|
||||
{capitalize(tab)}
|
||||
</Button>
|
||||
</GridItem>
|
||||
)
|
||||
}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Textarea } from '@chakra-ui/react'
|
||||
import { PropsOf } from '@chakra-ui/react'
|
||||
import autosize from 'autosize'
|
||||
import { useRef, useEffect } from 'react'
|
||||
|
||||
type TextareaAutosizeProps = PropsOf<typeof Textarea>
|
||||
|
||||
export const TextareaAutosize = (props: TextareaAutosizeProps): JSX.Element => {
|
||||
const ref = useRef()
|
||||
useEffect(() => {
|
||||
const current = ref.current
|
||||
autosize(current)
|
||||
return () => {
|
||||
autosize.destroy(current)
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<Textarea {...props} ref={ref} focusBorderColor='green.400' spellCheck={false} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Box, Button, Icon, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Flex, useDisclosure } from '@chakra-ui/react'
|
||||
import { Formik, Form } from 'formik'
|
||||
import { UserQuery, useUpdateInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface UpdateInfoButtonProps {
|
||||
data: UserQuery
|
||||
}
|
||||
|
||||
export const UpdateInfoButton = (props: UpdateInfoButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdateInfoMutation()
|
||||
const { isOpen: isEditingInfoOpen, onOpen: onEditingInfoOpen, onClose: onEditingInfoClose } = useDisclosure()
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onEditingInfoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingInfoOpen} onClose={onEditingInfoClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ fullName: data?.user?.fullName, headline: data?.user?.headline, address: data?.user?.address }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ input: values })
|
||||
onEditingInfoClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update info</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='fullName' placeholder='e.g. John Doe' label='Full Name' />
|
||||
<Box mt={4}>
|
||||
<InputField name='headline' placeholder='a short sentence about yourself' label='Headline' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='address' label='Address' />
|
||||
</Box>
|
||||
{/* <Box mt={4}>
|
||||
<InputField name='about' placeholder='A full introduction about yourself' label='About' inputType='quill' />
|
||||
</Box> */}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box, Button, Icon, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Flex, useDisclosure } from '@chakra-ui/react'
|
||||
import { Formik, Form } from 'formik'
|
||||
import { PageQuery, useUpdatePageInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface UpdatePageInfoButtonProps {
|
||||
data: PageQuery
|
||||
}
|
||||
|
||||
export const UpdatePageInfoButton = (props: UpdatePageInfoButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdatePageInfoMutation()
|
||||
const { isOpen: isEditingInfoOpen, onOpen: onEditingInfoOpen, onClose: onEditingInfoClose } = useDisclosure()
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onEditingInfoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingInfoOpen} onClose={onEditingInfoClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ fullPageName: data?.page?.fullPageName, headline: data?.page?.headline, address: data?.page?.address }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ id: data?.page?.id, input: values })
|
||||
onEditingInfoClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update info</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='fullPageName' placeholder='full name of your page' label='Full Name' />
|
||||
<Box mt={4}>
|
||||
<InputField name='headline' placeholder='a short sentence about your page' label='Headline' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='address' label='Address' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Box, Button, Icon, Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, ModalFooter, Flex, useDisclosure } from '@chakra-ui/react'
|
||||
import { Formik, Form } from 'formik'
|
||||
import { SpaceQuery, useUpdateSpaceInfoMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { InputField } from './InputField'
|
||||
|
||||
interface UpdateSpaceInfoButtonProps {
|
||||
data: SpaceQuery
|
||||
}
|
||||
|
||||
export const UpdateSpaceInfoButton = (props: UpdateSpaceInfoButtonProps): JSX.Element => {
|
||||
const { data } = props
|
||||
const [, updateInfo] = useUpdateSpaceInfoMutation()
|
||||
const { isOpen: isEditingInfoOpen, onOpen: onEditingInfoOpen, onClose: onEditingInfoClose } = useDisclosure()
|
||||
return (
|
||||
<>
|
||||
<Button variant='ghost' w='40px' h='40px' position='absolute' top={6} right='3rem' onClick={onEditingInfoOpen}>
|
||||
<Icon as={() => <Box className='fa-regular fa-pen' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isEditingInfoOpen} onClose={onEditingInfoClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ fullSpaceName: data?.space?.fullSpaceName, headline: data?.space?.headline }}
|
||||
onSubmit={async values => {
|
||||
await updateInfo({ id: data?.space?.id, input: values })
|
||||
onEditingInfoClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Update info</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputField name='fullSpaceName' placeholder='full name of your space' label='Full Name' />
|
||||
<Box mt={4}>
|
||||
<InputField name='headline' placeholder='a short sentence about your space' label='Headline' />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Box, Button, Flex, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useUploadCvMutation } from 'src/generated/graphql'
|
||||
import { ContainedButton } from './ContainedButton'
|
||||
import { FileField } from './FileField'
|
||||
|
||||
interface UploadCVButtonProps {
|
||||
|
||||
}
|
||||
|
||||
export const UploadCVButton = (props: UploadCVButtonProps): JSX.Element => {
|
||||
const { } = props
|
||||
const { isOpen: isUploadingCVOpen, onOpen: onUploadingCVOpen, onClose: onUploadingCVClose } = useDisclosure()
|
||||
const [, uploadCV] = useUploadCvMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onUploadingCVOpen} variant='ghost' w='40px' h='40px' position='absolute' top={10} right='3rem' >
|
||||
<Icon as={() => <Box className='fa-solid fa-plus' fontSize='1rem' />} />
|
||||
</Button>
|
||||
<Modal isOpen={isUploadingCVOpen} onClose={onUploadingCVClose} size='2xl'>
|
||||
<Formik
|
||||
initialValues={{ upload: null }}
|
||||
onSubmit={async (values) => {
|
||||
uploadCV(values)
|
||||
onUploadingCVClose()
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values }) =>
|
||||
<Form>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Upload new CV</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<FileField label='' name='upload' values={values} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Flex w='full' justify='center'>
|
||||
<ContainedButton baseColorLevel={500} type='submit' isLoading={isSubmitting} >
|
||||
Save
|
||||
</ContainedButton>
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Form>
|
||||
}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Box, Grid, Heading, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import { MeQuery, useCvsQuery, useDeleteCvMutation, useEducationItemsQuery, useExperiencesQuery, useQualificationsQuery, UserQuery } from 'src/generated/graphql'
|
||||
import { CreateEducationItemButton } from './CreateEducationItemButton'
|
||||
import { CreateExperienceButton } from './CreateExperienceButton'
|
||||
import { CreateQualificationButton } from './CreateQualificationButton'
|
||||
import { EditAboutButton } from './EditAboutButton'
|
||||
import { EducationItem } from './EducationItem'
|
||||
import { Experience } from './Experience'
|
||||
import { Qualification } from './Qualification'
|
||||
import { SetSkillsButton } from './SetSkillsButton'
|
||||
import { UploadCVButton } from './UploadCVButton'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface UserMainProps {
|
||||
data: UserQuery
|
||||
me: MeQuery['me']
|
||||
}
|
||||
|
||||
export const UserMain = (props: UserMainProps): JSX.Element => {
|
||||
const { data, me } = props
|
||||
const userId = useMemo(() => data?.user?.id, [data])
|
||||
const [{ data: educationItemsData }] = useEducationItemsQuery({
|
||||
pause: !userId,
|
||||
variables: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
const [{ data: experiencesData }] = useExperiencesQuery({
|
||||
pause: !userId,
|
||||
variables: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
const [{ data: qualificationsData }] = useQualificationsQuery({
|
||||
pause: !userId,
|
||||
variables: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
const [{ data: cvsData }] = useCvsQuery({
|
||||
pause: !userId,
|
||||
variables: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
const [, deleteCV] = useDeleteCvMutation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>About</Heading>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<EditAboutButton data={data} />
|
||||
}
|
||||
<QuillDisplay value={data?.user?.about} />
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>Education</Heading>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<CreateEducationItemButton />
|
||||
}
|
||||
{
|
||||
educationItemsData?.educationItems?.length > 0 &&
|
||||
<Grid templateRows={`repeat(${educationItemsData.educationItems.length}, 1fr)`} rowGap={3}>
|
||||
{
|
||||
educationItemsData.educationItems.map(item => (
|
||||
<EducationItem key={item.id} data={data} me={me} item={item} />
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
}
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>Experience</Heading>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<CreateExperienceButton />
|
||||
}
|
||||
{
|
||||
experiencesData?.experiences?.length > 0 &&
|
||||
<Grid templateRows={`repeat(${experiencesData.experiences.length}, 1fr)`} rowGap={3}>
|
||||
{
|
||||
experiencesData.experiences.map(item => (
|
||||
<Experience key={item.id} data={data} me={me} item={item} />
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
}
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>{'Licenses & Certifications'}</Heading>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<CreateQualificationButton />
|
||||
}
|
||||
{
|
||||
qualificationsData?.qualifications?.length > 0 &&
|
||||
<Grid templateRows={`repeat(${qualificationsData.qualifications.length}, 1fr)`} rowGap={3}>
|
||||
{
|
||||
qualificationsData.qualifications.map(item => (
|
||||
<Qualification key={item.id} data={data} me={me} item={item} />
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
}
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>Skills</Heading>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<SetSkillsButton data={data} />
|
||||
}
|
||||
{
|
||||
data?.user?.skills?.length > 0 &&
|
||||
data.user.skills.map((skill, index) => (
|
||||
<Tag key={index} mr={3} mb={2} colorScheme='green'>{skill}</Tag>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
<Box w='full' position='relative' bgColor='white' shadow='lg' mb='4' mt={10} p='10'>
|
||||
<Heading size='lg' mb={4}>CVs</Heading>
|
||||
{
|
||||
cvsData?.cvs?.length > 0 &&
|
||||
cvsData.cvs.map(cv => (
|
||||
<Tag
|
||||
key={cv.id}
|
||||
borderRadius='full'
|
||||
variant='subtle'
|
||||
colorScheme='green'
|
||||
>
|
||||
<TagLabel>
|
||||
<Link href={cv.url} download>
|
||||
{cv.filename}
|
||||
</Link>
|
||||
</TagLabel>
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<TagCloseButton onClick={() => { deleteCV({ id: cv.id }) }} />
|
||||
}
|
||||
</Tag>
|
||||
))
|
||||
}
|
||||
{
|
||||
data?.user?.id == me?.id &&
|
||||
<UploadCVButton />
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Box } from '@chakra-ui/react'
|
||||
import { Dispatch, SetStateAction, useState } from 'react'
|
||||
import { AppliedOffersQuery, OffersQuery } from 'src/generated/graphql'
|
||||
import { useUser } from 'src/hooks'
|
||||
import { AppliedOffers } from './AppliedOffers'
|
||||
import { PostedOffers } from './PostedOffers'
|
||||
import { TabButtons } from './TabButtons'
|
||||
|
||||
interface UserOffersProps {
|
||||
userId: number
|
||||
me: ReturnType<typeof useUser>
|
||||
offersData: OffersQuery
|
||||
offersFetching: boolean
|
||||
appliedOffersData: AppliedOffersQuery
|
||||
appliedOffersFetching: boolean
|
||||
offersVariables: { limit: number, cursor: string | null, userId: number }
|
||||
setOffersVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, userId: number }>>
|
||||
}
|
||||
|
||||
export const UserOffers = (props: UserOffersProps): JSX.Element => {
|
||||
const { userId, me, offersData, offersFetching, appliedOffersData, appliedOffersFetching, offersVariables, setOffersVariables } = props
|
||||
const [offersTab, setOffersTab] = useState('Posted')
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
userId == me?.id ?
|
||||
<>
|
||||
<Box mt={4}>
|
||||
<TabButtons tabs={['Applied', 'Posted']} defaultTab={offersTab} setActive={setOffersTab} />
|
||||
</Box>
|
||||
{
|
||||
offersTab == 'Posted' ?
|
||||
<PostedOffers me={me} offersData={offersData} offersFetching={offersFetching} offersVariables={offersVariables} setOffersVariables={setOffersVariables} />
|
||||
:
|
||||
<AppliedOffers appliedOffersData={appliedOffersData} appliedOffersFetching={appliedOffersFetching} me={me} />
|
||||
}
|
||||
</>
|
||||
:
|
||||
<PostedOffers me={me} offersData={offersData} offersFetching={offersFetching} offersVariables={offersVariables} setOffersVariables={setOffersVariables} />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Box, Button, Flex, Heading, Link, Stack, Tag, TagLabel, TagLeftIcon, Text } from '@chakra-ui/react'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dynamic from 'next/dynamic'
|
||||
import NextLink from 'next/link'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import NoSSR from 'react-no-ssr'
|
||||
import { PostsQuery } from 'src/generated/graphql'
|
||||
import { useCheckPageOwnership, useUser } from 'src/hooks'
|
||||
import { EditDeletePostButtons } from './EditDeletePostButtons'
|
||||
import { tagStylingMap } from './tagStylingMap'
|
||||
import { VoteSection } from './VoteSection'
|
||||
const QuillDisplay = dynamic(() => import('src/components/QuillDisplay.client'), { ssr: false })
|
||||
|
||||
interface UserPostsProps {
|
||||
postsData: PostsQuery
|
||||
postsFetching: boolean
|
||||
me: ReturnType<typeof useUser>
|
||||
postsVariables: { limit: number, cursor: string | null, userId: number }
|
||||
setPostsVariables: Dispatch<SetStateAction<{ limit: number, cursor: string | null, userId: number }>>
|
||||
}
|
||||
|
||||
export const UserPosts = (props: UserPostsProps): JSX.Element => {
|
||||
const { postsData, postsFetching, me, postsVariables, setPostsVariables } = props
|
||||
const checkPageOwnership = useCheckPageOwnership()
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!postsData && postsFetching ? <Box mt={4}>loading...</Box> :
|
||||
<Stack spacing={8} mb={8} mt={8}>
|
||||
{
|
||||
postsData.posts.posts.length > 0 &&
|
||||
postsData.posts.posts.map(p =>
|
||||
<Flex key={p.id} pl={5} pr={5} pt={3} pb={3} shadow='md' borderWidth='1px' bgColor='white' >
|
||||
<VoteSection post={cloneDeep(p)} />
|
||||
<Flex alignItems='center' flex={1}>
|
||||
<Box h='full'>
|
||||
<Flex fontSize='sm' mt={1.5} mb={1}>
|
||||
<Text>Posted</Text>
|
||||
<Text> in </Text>
|
||||
<NextLink passHref href='/s/[id]' as={`/s/${p?.space?.spaceName}`}>
|
||||
<Link>
|
||||
s/{p?.space?.spaceName}
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
<NextLink passHref href='/post/[id]' as={`/post/${p.id}`} >
|
||||
<Link>
|
||||
<Heading fontSize='xl' mb={2}>{p.title}</Heading>
|
||||
</Link>
|
||||
</NextLink>
|
||||
<Flex mb={3}>
|
||||
{
|
||||
p.tags?.map((t, index) => {
|
||||
const tagStyling = tagStylingMap[`${t.name}`]
|
||||
return (
|
||||
<Tag key={index} colorScheme={tagStyling.colorScheme} mr={2}>
|
||||
<Flex align='center'>
|
||||
{
|
||||
tagStyling.icon &&
|
||||
<Flex w='20px' justify='center' align='center' mr={1}>
|
||||
<TagLeftIcon as={() => tagStyling.icon} />
|
||||
</Flex>
|
||||
}
|
||||
<TagLabel color={tagStyling.textColor ? tagStyling.textColor : undefined}>{t.name}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
<NoSSR onSSR='Loading...'>
|
||||
<QuillDisplay value={p.text} />
|
||||
</NoSSR>
|
||||
</Box>
|
||||
{
|
||||
((me?.id && me.id == p?.creator?.id) || checkPageOwnership(p?.pageCreator?.id)) &&
|
||||
<Flex flexDir='column' justifyContent='space-between' ml='auto' height='calc(80px + 0.25rem)'>
|
||||
<EditDeletePostButtons id={p.id} />
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
}
|
||||
{
|
||||
postsData?.posts?.hasMore &&
|
||||
<Flex>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPostsVariables({
|
||||
limit: postsVariables.limit,
|
||||
cursor: postsData.posts.posts[postsData.posts.posts.length - 1].createdAt,
|
||||
userId: postsVariables.userId
|
||||
})
|
||||
}}
|
||||
isLoading={postsFetching}
|
||||
m='auto'
|
||||
my={8}
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ArrowDownIcon, ArrowUpIcon } from '@chakra-ui/icons'
|
||||
import { Flex, IconButton } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { RegularPostFragment, useVoteMutation } from 'src/generated/graphql'
|
||||
import { useRequireLogin } from 'src/hooks'
|
||||
|
||||
interface VoteSectionProps {
|
||||
post: RegularPostFragment
|
||||
}
|
||||
|
||||
export const VoteSection = (props: VoteSectionProps): JSX.Element => {
|
||||
const { post: p } = props
|
||||
const [loadingState, setLoadingState] = useState<'upvote-loading' | 'downvote-loading' | 'not-loading'>
|
||||
('not-loading')
|
||||
const [, vote] = useVoteMutation()
|
||||
const requireLogin = useRequireLogin()
|
||||
return (
|
||||
<Flex flexDir='column' alignItems='center' justifyContent='center' mr={4}>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setLoadingState('upvote-loading')
|
||||
requireLogin()
|
||||
await vote({
|
||||
postId: p.id,
|
||||
value: 1,
|
||||
})
|
||||
setLoadingState('not-loading')
|
||||
}}
|
||||
isLoading={loadingState == 'upvote-loading'}
|
||||
icon={
|
||||
<ArrowUpIcon boxSize='22px' />
|
||||
}
|
||||
color={p.voteStatus == 1 ? 'green' : ''}
|
||||
bg='transparent'
|
||||
aria-label='upvote'
|
||||
/>
|
||||
{p.points}
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
setLoadingState('downvote-loading')
|
||||
requireLogin()
|
||||
await vote({
|
||||
postId: p.id,
|
||||
value: -1,
|
||||
})
|
||||
setLoadingState('not-loading')
|
||||
}}
|
||||
isLoading={loadingState == 'downvote-loading'}
|
||||
icon={
|
||||
<ArrowDownIcon boxSize='22px' />
|
||||
}
|
||||
color={p.voteStatus == -1 ? 'tomato' : ''}
|
||||
bg='transparent'
|
||||
aria-label='downvote'
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Box } from '@chakra-ui/react'
|
||||
|
||||
export type WrapperVariant = 'small' | 'regular' | 'large' | 'full'
|
||||
|
||||
interface WrapperProps {
|
||||
children?: React.ReactNode
|
||||
variant?: WrapperVariant
|
||||
}
|
||||
|
||||
const variantToWidth = (variant: WrapperVariant): string => {
|
||||
switch (variant) {
|
||||
case 'small':
|
||||
return '400px'
|
||||
case 'regular':
|
||||
return '800px'
|
||||
case 'large':
|
||||
return '1200px'
|
||||
case 'full':
|
||||
return '100vw'
|
||||
}
|
||||
}
|
||||
|
||||
export const Wrapper = (props: WrapperProps): JSX.Element => {
|
||||
const { children, variant = 'regular' } = props
|
||||
return (
|
||||
<Box mt='50px' pt={8} mb={8} maxW={variantToWidth(variant)} w='100%' mx='auto'>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
export * from './InputField'
|
||||
export * from './NoUnderlineLink'
|
||||
export * from './Layout'
|
||||
export * from './NavBar'
|
||||
export * from './Logo'
|
||||
export * from './LogoButton'
|
||||
export * from './OutlinedButton'
|
||||
export * from './ContainedButton'
|
||||
export * from './TabButtons'
|
||||
export * from './Wrapper'
|
||||
export * from './VoteSection'
|
||||
export * from './EditDeletePostButtons'
|
||||
export * from './FormSuccessMessage'
|
||||
export * from './CommentVoteSection'
|
||||
export * from './EditDeleteCommentButtons'
|
||||
export * from './SwitchField'
|
||||
export * from './EditDeleteOfferButtons'
|
||||
export * from './Interpunct'
|
||||
export * from './SelectField'
|
||||
export * from './tagStylingMap'
|
||||
export * from './selectConfigs'
|
||||
export * from './EditDeletePageButtons'
|
||||
export * from './EditDeleteSpaceButtons'
|
||||
export * from './UpdateInfoButton'
|
||||
export * from './ChangeCoverPhotoButton'
|
||||
export * from './ChangeAvatarButton'
|
||||
export * from './EditAboutButton'
|
||||
export * from './DatePickerField'
|
||||
export * from './PhotoField'
|
||||
export * from './CreateEducationItemButton'
|
||||
export * from './EducationItem'
|
||||
export * from './CreateExperienceButton'
|
||||
export * from './Experience'
|
||||
export * from './FollowUserButton'
|
||||
export * from './Qualification'
|
||||
export * from './CreatableSelectField'
|
||||
export * from './SetSkillsButton'
|
||||
export * from './FileField'
|
||||
export * from './UploadCVButton'
|
||||
export * from './Profile'
|
||||
export * from './ChangePageCoverPhotoButton'
|
||||
export * from './UpdatePageInfoButton'
|
||||
export * from './ChangePageAvatarButton'
|
||||
export * from './FollowPageButton'
|
||||
export * from './PageMain'
|
||||
export * from './EditPageAboutButton'
|
||||
export * from './ChangeSpaceCoverPhotoButton'
|
||||
export * from './UpdateSpaceInfoButton'
|
||||
export * from './ChangeSpaceAvatarButton'
|
||||
export * from './SubscribeSpaceButton'
|
||||
export * from './SpaceMain'
|
||||
export * from './EditSpaceAboutButton'
|
||||
export * from './TextareaAutosize'
|
||||
export * from './InboxItem'
|
||||
export * from './CreateConversationButton'
|
||||
export * from './Message'
|
||||
export * from './IndexPosts'
|
||||
export * from './IndexOffers'
|
||||
export * from './PageProfile'
|
||||
export * from './PagePosts'
|
||||
export * from './PageOffers'
|
||||
export * from './SpaceProfile'
|
||||
export * from './SpacePosts'
|
||||
export * from './SpaceOffers'
|
||||
export * from './PostedOffers'
|
||||
export * from './AppliedOffers'
|
||||
export * from './UserMain'
|
||||
export * from './UserPosts'
|
||||
export * from './UserOffers'
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Box, Tag, Flex, TagLeftIcon, TagLabel } from '@chakra-ui/react'
|
||||
import { chakraComponents, Select, OptionProps, GroupBase, MultiValueGenericProps } from 'chakra-react-select'
|
||||
import { SelectOption } from 'src/types'
|
||||
const { Option, MultiValueContainer } = chakraComponents
|
||||
|
||||
export const tagSelectCustomComponents = {
|
||||
Option: ({
|
||||
children,
|
||||
...props
|
||||
}: OptionProps<SelectOption, boolean, GroupBase<SelectOption>>) => (
|
||||
<Option {...props}>
|
||||
<Tag colorScheme={props.data.colorScheme ? props.data.colorScheme : undefined}>
|
||||
<Flex align='center'>
|
||||
{
|
||||
props.data.icon &&
|
||||
<Flex w='20px' justify='center' align='center' mr={1}>
|
||||
<TagLeftIcon as={() => props.data.icon} />
|
||||
</Flex>
|
||||
}
|
||||
<TagLabel color={props.data.textColor ? props.data.textColor : undefined}>{children}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
</Option>
|
||||
),
|
||||
MultiValueContainer: ({
|
||||
children,
|
||||
...props
|
||||
}: MultiValueGenericProps<SelectOption, boolean, GroupBase<SelectOption>>) => (
|
||||
<MultiValueContainer {...props}>
|
||||
{
|
||||
props.data.icon &&
|
||||
<Box mr={2}>
|
||||
<TagLeftIcon as={() => props.data.icon} />
|
||||
</Box>
|
||||
}
|
||||
{children}
|
||||
</MultiValueContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons'
|
||||
import { Box } from '@chakra-ui/react'
|
||||
import { SelectOption } from 'src/types'
|
||||
|
||||
export const tagStylingMap: Record<string, Omit<SelectOption, 'label' | 'value'>> = {
|
||||
Question: {
|
||||
colorScheme: 'blackAlpha',
|
||||
icon: <QuestionOutlineIcon />
|
||||
},
|
||||
Help: {
|
||||
colorScheme: 'messenger',
|
||||
icon: <Box className='fa-solid fa-comment' />
|
||||
},
|
||||
Fundraising: {
|
||||
colorScheme: 'green',
|
||||
icon: <Box className='fa-solid fa-dollar-sign' />
|
||||
},
|
||||
Introduction: {
|
||||
colorScheme: 'red',
|
||||
icon: <Box className='fa-solid fa-info' />
|
||||
},
|
||||
Self: {
|
||||
colorScheme: 'telegram',
|
||||
icon: <Box className='fa-solid fa-address-card' />
|
||||
},
|
||||
AMA: {
|
||||
colorScheme: 'orange',
|
||||
icon: <Box className='fa-solid fa-comments-question' />
|
||||
},
|
||||
Resources: {
|
||||
colorScheme: 'teal',
|
||||
icon: <Box className='fa-solid fa-books' />
|
||||
},
|
||||
Educational: {
|
||||
colorScheme: 'purple',
|
||||
icon: <Box className='fa-solid fa-building-columns' />
|
||||
},
|
||||
Poll: {
|
||||
colorScheme: 'pink',
|
||||
icon: <Box className='fa-solid fa-box-ballot' />
|
||||
},
|
||||
Recruitment: {
|
||||
colorScheme: 'green',
|
||||
icon: <Box className='fa-solid fa-bullseye-arrow' />
|
||||
},
|
||||
Contract: {
|
||||
colorScheme: 'yellow',
|
||||
icon: <Box className='fa-solid fa-handshake-simple' />
|
||||
},
|
||||
Networking: {
|
||||
colorScheme: 'facebook',
|
||||
icon: <Box className='fa-solid fa-chart-network' />
|
||||
},
|
||||
Opinion: {
|
||||
colorScheme: 'twitter',
|
||||
icon: <Box className='fa-solid fa-message-lines' />
|
||||
},
|
||||
Other: {
|
||||
colorScheme: 'gray'
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user