This commit is contained in:
2026-06-24 14:07:11 +02:00
commit cc4de1d450
296 changed files with 51110 additions and 0 deletions
+58
View File
@@ -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>
}
</>
)
}
+191
View File
@@ -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&apos;s avatar</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>User&apos;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>
</>
)
}
+188
View File
@@ -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&apos;s cover photo</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>User&apos;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>
</>
)
}
+196
View File
@@ -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&apos;s avatar</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>Page&apos;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&apos;s cover photo</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>page&apos;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>
</>
)
}
+197
View File
@@ -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&apos;s avatar</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>Space&apos;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&apos;s cover photo</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text fontWeight='bold' mb={2}>Space&apos;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>
</>
)
}
+59
View File
@@ -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>
)
}
+23
View File
@@ -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>
)
}
+33
View File
@@ -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>
</>
)
}
+72
View File
@@ -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>
</>
)
}
+54
View File
@@ -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>
)
}
+52
View File
@@ -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 })
}}
/>
</>
)
}
+36
View File
@@ -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 })
}}
/>
</>
)
}
+37
View File
@@ -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 })
}}
/>
</>
)
}
+36
View File
@@ -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 })
}}
/>
</>
)
}
+37
View File
@@ -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 })
}}
/>
</>
)
}
+53
View File
@@ -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>
</>
)
}
+52
View File
@@ -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>
</>
)
}
+52
View File
@@ -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>
</>
)
}
+100
View File
@@ -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>
)
}
+100
View File
@@ -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>
)
}
+65
View File
@@ -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>
)
}
+41
View File
@@ -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>
)
}
+41
View File
@@ -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>
)
}
+17
View File
@@ -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>
)
}
+90
View File
@@ -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>
)
}
+93
View File
@@ -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&nbsp;</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>&nbsp;in&nbsp;</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>
}
</>
)
}
+118
View File
@@ -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&nbsp;</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>&nbsp;in&nbsp;</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>
}
</>
)
}
+120
View File
@@ -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} />
}
</>
)
}
+7
View File
@@ -0,0 +1,7 @@
import { Text } from '@chakra-ui/react'
export const Interpunct = (): JSX.Element => {
return (
<Text>&nbsp;·&nbsp;</Text>
)
}
+21
View File
@@ -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>
)
}
+13
View File
@@ -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} />
)
}
+25
View File
@@ -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}
/>
)
}
+104
View File
@@ -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>
)
}
+137
View File
@@ -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>
)
}
+21
View File
@@ -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>
)
}
+25
View File
@@ -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>
)
}
+28
View File
@@ -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>
</>
)
}
+84
View File
@@ -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&nbsp;
<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>
}
</>
)
}
+111
View File
@@ -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>&nbsp;in&nbsp;</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>
}
</>
)
}
+74
View File
@@ -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'>&nbsp;followers</Text>
</Flex>
</Flex>
</Flex>
</Box>
)
}
+125
View File
@@ -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>
)
}
+84
View File
@@ -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&nbsp;
<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>
}
</>
)
}
+93
View File
@@ -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'>&nbsp;followers</Text>
</Flex>
<Interpunct />
<Flex>
<Text color='gray.700'>{Number(data?.user?.followingNumber).toLocaleString()}</Text>
<Text color='gray.600'>&nbsp;following</Text>
</Flex>
</Flex>
</Flex>
</Box>
)
}
+117
View File
@@ -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 ?
<>&nbsp;Expires on {DateTime.fromISO(item.issuanceDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT)}</>
:
<>&nbsp;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>
)
}
+43
View File
@@ -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
+33
View File
@@ -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
+44
View File
@@ -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>
)
}
+56
View File
@@ -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>
</>
)
}
+36
View File
@@ -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>
</>
)
}
+88
View File
@@ -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&nbsp;</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>
}
</>
)
}
+112
View File
@@ -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&nbsp;</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>
}
</>
)
}
+73
View File
@@ -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'>&nbsp;subscribers</Text>
</Flex>
</Flex>
</Flex>
</Box>
)
}
+41
View File
@@ -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>
)
}
+36
View File
@@ -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>
)
}
+38
View File
@@ -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>
)
}
+20
View File
@@ -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} />
)
}
+60
View File
@@ -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>
</>
)
}
+57
View File
@@ -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>
</>
)
}
+54
View File
@@ -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>
</>
)
}
+52
View File
@@ -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>
</>
)
}
+155
View File
@@ -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>
</>
)
}
+44
View File
@@ -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} />
}
</>
)
}
+109
View File
@@ -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>&nbsp;in&nbsp;</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>
}
</>
)
}
+58
View File
@@ -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>
)
}
+30
View File
@@ -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>
)
}
+69
View File
@@ -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'
+39
View File
@@ -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>
)
}
+61
View File
@@ -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'
},
}