commit cc4de1d4509cd9190236f7863ae6100d31ca61ec Author: [Quy Anh] «Elliot» Nguyen Date: Wed Jun 24 14:07:11 2026 +0200 --- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b3f7753 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true +[*] +quote_type = single +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1084bba --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +NEXT_PUBLIC_S3_HOSTNAME= +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= +NEXT_PUBLIC_FIREBASE_PROJECT_ID= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= +NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= +NEXT_PUBLIC_API_URL= +NEXT_PUBLIC_SERVER_NON_API_ERROR_REGEX= + +# Only needed when deploying using Serverless +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a91e0b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# serverless +.serverless +.serverless_nextjs + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..6b3175d --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,3 @@ +nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-4.5.0.cjs diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa96f71 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Frontend for Comroots diff --git a/codegen.yml b/codegen.yml new file mode 100644 index 0000000..5e221c6 --- /dev/null +++ b/codegen.yml @@ -0,0 +1,9 @@ +overwrite: true +schema: "http://localhost:4000/graphql" +documents: "src/graphql/**/*.graphql" +generates: + src/generated/graphql.tsx: + plugins: + - "typescript" + - "typescript-operations" + - "typescript-urql" diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..7646991 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,20 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + NEXT_PUBLIC_S3_HOSTNAME: string; + NEXT_PUBLIC_FIREBASE_API_KEY: string; + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: string; + NEXT_PUBLIC_FIREBASE_PROJECT_ID: string; + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: string; + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: string; + NEXT_PUBLIC_FIREBASE_APP_ID: string; + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: string; + NEXT_PUBLIC_API_URL: string; + NEXT_PUBLIC_SERVER_NON_API_ERROR_REGEX: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + } + } +} + +export {} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..580f4fe --- /dev/null +++ b/next.config.js @@ -0,0 +1,23 @@ +require('dotenv').config({ + path: './.env.local' +}) + +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: false, + images: { + domains: [process.env.NEXT_PUBLIC_S3_HOSTNAME] + }, + eslint: { + ignoreDuringBuilds: true + }, + typescript: { + ignoreBuildErrors: true + } +} + +module.exports = withBundleAnalyzer(nextConfig) diff --git a/package.json b/package.json new file mode 100644 index 0000000..e516752 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "comroots-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "analyze": "ANALYZE=true run build", + "start": "next start", + "lint": "next lint", + "env:generate": "gen-env-types .env.local -o env.d.ts -e .", + "gqlcg": "graphql-codegen --config codegen.yml", + "deploy": "sh ./scripts/deploy.sh" + }, + "dependencies": { + "@chakra-ui/form-control": "^2.2.0", + "@chakra-ui/hooks": "^2.0.4", + "@chakra-ui/icons": "^2.0.3", + "@chakra-ui/layout": "^2.3.1", + "@chakra-ui/media-query": "^3.3.0", + "@chakra-ui/menu": "^2.2.1", + "@chakra-ui/react": "^2.2.4", + "@chakra-ui/spinner": "^2.1.0", + "@chakra-ui/system": "^2.6.2", + "@emotion/hash": "^0.9.0", + "@emotion/react": "^11.10.0", + "@emotion/server": "^11.10.0", + "@emotion/styled": "^11.9.3", + "@mantine/core": "^5.0.3", + "@mantine/dates": "^5.0.3", + "@mantine/hooks": "^5.0.3", + "@mantine/next": "^5.0.3", + "@mui/material": "^5.9.0", + "@next/bundle-analyzer": "^12.2.5", + "@urql/exchange-graphcache": "^4.4.3", + "@urql/exchange-multipart-fetch": "^0.1.14", + "autosize": "^5.0.1", + "chakra-react-select": "^4.1.4", + "critters": "^0.0.16", + "dayjs": "^1.11.4", + "dotenv": "^16.0.1", + "firebase": "^9.9.1", + "formik": "^2.2.9", + "framer-motion": "^6.4.3", + "graphql": "^16.5.0", + "lodash": "^4.17.21", + "luxon": "^3.0.1", + "next": "12.2.2", + "next-urql": "^3.3.3", + "quill-blot-formatter": "1.0.5", + "react": "18.2.0", + "react-avatar-editor": "^13.0.0", + "react-dom": "18.2.0", + "react-dropzone": "^14.2.2", + "react-firebase-hooks": "^5.0.3", + "react-is": "^18.2.0", + "react-no-ssr": "^1.1.0", + "react-quill": "^2.0.0", + "urql": "^2.2.2", + "wonka": "^4.0.15" + }, + "devDependencies": { + "@graphql-codegen/cli": "^2.8.0", + "@graphql-codegen/typescript": "^2.7.1", + "@graphql-codegen/typescript-operations": "^2.5.1", + "@graphql-codegen/typescript-urql": "^3.6.1", + "@types/autosize": "^4.0.1", + "@types/dotenv-safe": "^8.1.2", + "@types/lodash": "^4.14.182", + "@types/luxon": "^3.0.0", + "@types/node": "18.0.3", + "@types/react": "18.0.15", + "@types/react-avatar-editor": "^12.0.0", + "@types/react-dom": "18.0.6", + "@types/react-dropzone": "^5.1.0", + "@types/react-no-ssr": "^1.1.3", + "depcheck": "^1.4.3", + "eslint": "8.19.0", + "eslint-config-next": "12.2.2", + "gen-env-types": "^1.3.4", + "typescript": "4.7.4" + }, + "packageManager": "yarn@4.5.0" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4a37c45 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/logos/logo.100.webp b/public/logos/logo.100.webp new file mode 100644 index 0000000..7065e50 Binary files /dev/null and b/public/logos/logo.100.webp differ diff --git a/public/logos/logo.200.webp b/public/logos/logo.200.webp new file mode 100644 index 0000000..e1672f8 Binary files /dev/null and b/public/logos/logo.200.webp differ diff --git a/public/logos/logo.300.webp b/public/logos/logo.300.webp new file mode 100644 index 0000000..8a9bbaa Binary files /dev/null and b/public/logos/logo.300.webp differ diff --git a/public/logos/logo.400.webp b/public/logos/logo.400.webp new file mode 100644 index 0000000..b4b525f Binary files /dev/null and b/public/logos/logo.400.webp differ diff --git a/public/logos/logo.50.webp b/public/logos/logo.50.webp new file mode 100644 index 0000000..d1fecba Binary files /dev/null and b/public/logos/logo.50.webp differ diff --git a/public/logos/logo.500.webp b/public/logos/logo.500.webp new file mode 100644 index 0000000..f727636 Binary files /dev/null and b/public/logos/logo.500.webp differ diff --git a/public/logos/logo.600.webp b/public/logos/logo.600.webp new file mode 100644 index 0000000..feb5111 Binary files /dev/null and b/public/logos/logo.600.webp differ diff --git a/public/logos/logo.700.webp b/public/logos/logo.700.webp new file mode 100644 index 0000000..8ee1dd5 Binary files /dev/null and b/public/logos/logo.700.webp differ diff --git a/public/logos/logo.800.webp b/public/logos/logo.800.webp new file mode 100644 index 0000000..eb127f8 Binary files /dev/null and b/public/logos/logo.800.webp differ diff --git a/public/logos/logo.900.webp b/public/logos/logo.900.webp new file mode 100644 index 0000000..9b6b08c Binary files /dev/null and b/public/logos/logo.900.webp differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..76f6421 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# Allow all crawlers +User-agent: * +Allow: / \ No newline at end of file diff --git a/public/samples/coverPhoto.webp b/public/samples/coverPhoto.webp new file mode 100644 index 0000000..e3af4fa Binary files /dev/null and b/public/samples/coverPhoto.webp differ diff --git a/public/samples/square.webp b/public/samples/square.webp new file mode 100644 index 0000000..96bb73b Binary files /dev/null and b/public/samples/square.webp differ diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..9dc7ab3 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,8 @@ +# lambda@edge (using serverless framework) +# . ./.env.local +# export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID +# export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY +# components-v1 + +# vercel +vercel --prod diff --git a/serverless.yml b/serverless.yml new file mode 100644 index 0000000..9962d52 --- /dev/null +++ b/serverless.yml @@ -0,0 +1,4 @@ +# serverless.yml + +comroots: + component: "@sls-next/serverless-component@3.7.0" diff --git a/src/components/AppliedOffers.tsx b/src/components/AppliedOffers.tsx new file mode 100644 index 0000000..3049d0f --- /dev/null +++ b/src/components/AppliedOffers.tsx @@ -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 + appliedOffersData: AppliedOffersQuery + appliedOffersFetching: boolean +} + +export const AppliedOffers = (props: AppliedOffersProps): JSX.Element => { + const { me, appliedOffersData, appliedOffersFetching } = props + const checkPageOwnership = useCheckPageOwnership() + + return ( + <> + { + !appliedOffersData && appliedOffersFetching ? loading... : + + { + appliedOffersData.appliedOffers.length > 0 && + appliedOffersData.appliedOffers.map(o => + o && + + + offer-photo + + + + {o.title} + + + Posted by {o.creatorType == 'user' ? o.creator.username : o.pageCreator.pageName} in s/{o.space?.spaceName} + {o.workplace} + {o.address} + {o.recruiting ? 'Recruiting' : 'Not recruiting'} + {capitalize(o.applicationStatus)} + {DateTime.fromMillis(parseInt(o.createdAt)).toRelative()} + + { + ((me?.id && me.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) && + + + + } + + + ) + } + + } + + ) +} \ No newline at end of file diff --git a/src/components/ChangeAvatarButton.tsx b/src/components/ChangeAvatarButton.tsx new file mode 100644 index 0000000..5ee4d34 --- /dev/null +++ b/src/components/ChangeAvatarButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState | 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 ( + <> + + Change Avatar + + + + + Change user's avatar + + + User's avatar collection: + + { + avatarsData?.avatars?.length > 0 && + avatarsData.avatars.map((photo, index) => ( + photo && + + + avatar { + setSelected(photo.key) + }} + h='40px' + w='40px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={1} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setAvatarKey(selected) + await changeAvatar({ + key: selected, + }) + onChangingAvatarClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ChangeCoverPhotoButton.tsx b/src/components/ChangeCoverPhotoButton.tsx new file mode 100644 index 0000000..6131c27 --- /dev/null +++ b/src/components/ChangeCoverPhotoButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState>(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 ( + <> + + + + + Change user's cover photo + + + User's cover photo collection: + + { + coverPhotosData?.coverPhotos?.length > 0 && + coverPhotosData.coverPhotos.map((photo, index) => ( + photo && + + + cover-photo { + setSelected(photo.key) + }} + h='40px' + w='210.5px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={0.5} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setCoverPhotoKey(selected) + await changeCoverPhoto({ + key: selected, + }) + onChangingCoverPhotoClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ChangePageAvatarButton.tsx b/src/components/ChangePageAvatarButton.tsx new file mode 100644 index 0000000..7235fee --- /dev/null +++ b/src/components/ChangePageAvatarButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState>(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 ( + <> + + Change Avatar + + + + + Change page's avatar + + + Page's avatar collection: + + { + avatarsData?.pageAvatars?.length > 0 && + avatarsData.pageAvatars.map((photo, index) => ( + photo && + + + avatar { + setSelected(photo.key) + }} + h='40px' + w='40px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={1} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setAvatarKey(selected) + await changePageAvatar({ + pageId, + key: selected, + }) + onChangingAvatarClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ChangePageCoverPhotoButton.tsx b/src/components/ChangePageCoverPhotoButton.tsx new file mode 100644 index 0000000..56a54c1 --- /dev/null +++ b/src/components/ChangePageCoverPhotoButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState>(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 ( + <> + + + + + Change page's cover photo + + + page's cover photo collection: + + { + coverPhotosData?.pageCoverPhotos?.length > 0 && + coverPhotosData.pageCoverPhotos.map((photo, index) => ( + photo && + + + cover-photo { + setSelected(photo.key) + }} + h='40px' + w='210.5px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={0.5} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setCoverPhotoKey(selected) + await changeCoverPhoto({ + pageId, + key: selected, + }) + onChangingCoverPhotoClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ChangeSpaceAvatarButton.tsx b/src/components/ChangeSpaceAvatarButton.tsx new file mode 100644 index 0000000..4100b01 --- /dev/null +++ b/src/components/ChangeSpaceAvatarButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState>(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 ( + <> + + Change Avatar + + + + + Change space's avatar + + + Space's avatar collection: + + { + avatarsData?.spaceAvatars?.length > 0 && + avatarsData.spaceAvatars.map((photo, index) => ( + photo && + + + avatar { + setSelected(photo.key) + }} + h='40px' + w='40px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={1} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setAvatarKey(selected) + await changeSpaceAvatar({ + spaceId, + key: selected, + }) + onChangingAvatarClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/ChangeSpaceCoverPhotoButton.tsx b/src/components/ChangeSpaceCoverPhotoButton.tsx new file mode 100644 index 0000000..5613057 --- /dev/null +++ b/src/components/ChangeSpaceCoverPhotoButton.tsx @@ -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> +} + +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(null) + const [editorOptions, setEditorOptions] = useState>(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 ( + <> + + + + + Change space's cover photo + + + Space's cover photo collection: + + { + coverPhotosData?.spaceCoverPhotos?.length > 0 && + coverPhotosData.spaceCoverPhotos.map((photo, index) => ( + photo && + + + cover-photo { + setSelected(photo.key) + }} + h='40px' + w='210.5px' + border={selected != photo.key ? '3px solid transparent' : '3px solid green'} + /> + + + )) + } + + { + editorOptions ? + + { setEditorOptions({ ...editorOptions, position }) }} + {...editorOptions} + /> + You can select the part of the image to be included with your mouse. + + Zoom + { setEditorOptions({ ...editorOptions, scale: value }) }} + min={0.5} + max={2} + step={0.01} + > + + + + + + + + Rotate + { setEditorOptions({ ...editorOptions, rotate: value }) }} + min={0} + max={180} + > + + + + + + + { + 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 + + + : + + { setEditorOptions({ image: acceptedFiles[0] }) }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your image here ...' : + 'Drag \'n\' drop an image here, or click to select one' + } + +
+ )} +
+
+ } +
+ + + { + setCoverPhotoKey(selected) + await changeCoverPhoto({ + spaceId, + key: selected, + }) + onChangingCoverPhotoClose() + }} + baseColorLevel={500} + > + Save + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/CommentVoteSection.tsx b/src/components/CommentVoteSection.tsx new file mode 100644 index 0000000..6d50f73 --- /dev/null +++ b/src/components/CommentVoteSection.tsx @@ -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 ( + + { + setLoadingState('upvote-loading') + requireLogin() + await voteComment({ + commentId: c.id, + value: 1, + }) + setLoadingState('not-loading') + }} + isLoading={loadingState == 'upvote-loading'} + icon={ + + } + color={c.voteStatus == 1 ? 'green' : ''} + bg='transparent' + aria-label='upvote' + /> + {c.points} + { + setLoadingState('downvote-loading') + requireLogin() + await voteComment({ + commentId: c.id, + value: -1, + }) + setLoadingState('not-loading') + }} + isLoading={loadingState == 'downvote-loading'} + icon={ + + } + color={c.voteStatus == -1 ? 'tomato' : ''} + bg='transparent' + aria-label='downvote' + /> + + ) +} \ No newline at end of file diff --git a/src/components/ContainedButton.tsx b/src/components/ContainedButton.tsx new file mode 100644 index 0000000..76dc720 --- /dev/null +++ b/src/components/ContainedButton.tsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/src/components/CreatableSelectField.tsx b/src/components/CreatableSelectField.tsx new file mode 100644 index 0000000..fc5ba21 --- /dev/null +++ b/src/components/CreatableSelectField.tsx @@ -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 & { + 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 ( + + {label} + { setValue(newValue.map(v => v.value)) }} + {...props} + /> + {error && {error}} + + ) +} \ No newline at end of file diff --git a/src/components/CreateConversationButton.tsx b/src/components/CreateConversationButton.tsx new file mode 100644 index 0000000..5802903 --- /dev/null +++ b/src/components/CreateConversationButton.tsx @@ -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 +} + +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 ( + <> + + } /> + + { + 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 }) => +
+ + + Create new conversation + + + + { + showSuccessMessage && + + } + + + + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/CreateEducationItemButton.tsx b/src/components/CreateEducationItemButton.tsx new file mode 100644 index 0000000..e31d2e5 --- /dev/null +++ b/src/components/CreateEducationItemButton.tsx @@ -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 ( + <> + + + { + const response = await createEducationItem({ input: values }) + if (response.data?.createEducationItem?.errors) { + setErrors(toErrorMap(response.data.createEducationItem.errors)) + } + else { + onAddingEducationItemClose() + } + }} + > + {({ isSubmitting }) => +
+ + + Add education + + + + + + + + + + + + + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/CreateExperienceButton.tsx b/src/components/CreateExperienceButton.tsx new file mode 100644 index 0000000..e4d5119 --- /dev/null +++ b/src/components/CreateExperienceButton.tsx @@ -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 ( + <> + + + { + const response = await createExperience({ input: values }) + if (response.data?.createExperience?.errors) { + setErrors(toErrorMap(response.data.createExperience.errors)) + } + else { + onAddingExperienceClose() + } + }} + > + {({ isSubmitting }) => +
+ + + Add experience + + + + + + + + + + + + + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/CreateQualificationButton.tsx b/src/components/CreateQualificationButton.tsx new file mode 100644 index 0000000..898dd0d --- /dev/null +++ b/src/components/CreateQualificationButton.tsx @@ -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 ( + <> + + + { + const response = await createQualification({ input: values }) + if (response.data?.createQualification?.errors) { + setErrors(toErrorMap(response.data.createQualification.errors)) + } + else { + onAddingQualificationClose() + } + }} + > + {({ isSubmitting }) => +
+ + + {'Add license or certification'} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/DatePickerField.tsx b/src/components/DatePickerField.tsx new file mode 100644 index 0000000..7a14c4b --- /dev/null +++ b/src/components/DatePickerField.tsx @@ -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 & { + 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(null) + + return ( + + {label} + { 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 && {error}} + + ) +} \ No newline at end of file diff --git a/src/components/EditAboutButton.tsx b/src/components/EditAboutButton.tsx new file mode 100644 index 0000000..afb5af6 --- /dev/null +++ b/src/components/EditAboutButton.tsx @@ -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 ( + <> + + + { + await updateInfo({ input: values }) + onEditingAboutClose() + }} + > + {({ isSubmitting }) => +
+ + + Update about + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/EditDeleteCommentButtons.tsx b/src/components/EditDeleteCommentButtons.tsx new file mode 100644 index 0000000..6115206 --- /dev/null +++ b/src/components/EditDeleteCommentButtons.tsx @@ -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 +} + +export const EditDeleteCommentButtons = (props: EditDeleteCommentButtonsProps): JSX.Element => { + const { comment } = props + const [, deleteComment] = useDeleteCommentMutation() + const [, updateComment] = useUpdateCommentMutation() + const { isOpen, onOpen, onClose } = useDisclosure() + return ( + <> + } + aria-label='edit' + onClick={onOpen} + /> + + { + await updateComment(values) + onClose() + }} + > + {({ isSubmitting }) => +
{ }}> + + + Comment + + + + + + + + Save + + + + + + } +
+
+ } + aria-label='delete' + onClick={async () => { + await deleteComment({ id: comment.id }) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/components/EditDeleteOfferButtons.tsx b/src/components/EditDeleteOfferButtons.tsx new file mode 100644 index 0000000..e86e570 --- /dev/null +++ b/src/components/EditDeleteOfferButtons.tsx @@ -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 ( + <> + + } + aria-label='edit' + as={NoUnderlineLink} + /> + + } + aria-label='delete' + onClick={async () => { + await deleteOffer({ id }) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/components/EditDeletePageButtons.tsx b/src/components/EditDeletePageButtons.tsx new file mode 100644 index 0000000..5f28fbd --- /dev/null +++ b/src/components/EditDeletePageButtons.tsx @@ -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 +} + +export const EditDeletePageButtons = (props: EditDeletePageButtonsProps): JSX.Element => { + const { page } = props + const [, deletePage] = useDeletePageMutation() + + return ( + <> + + } + aria-label='edit' + as={NoUnderlineLink} + /> + + } + aria-label='delete' + onClick={async () => { + await deletePage({ id: page.id }) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/components/EditDeletePostButtons.tsx b/src/components/EditDeletePostButtons.tsx new file mode 100644 index 0000000..8388468 --- /dev/null +++ b/src/components/EditDeletePostButtons.tsx @@ -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 ( + <> + + } + aria-label='edit' + as={NoUnderlineLink} + /> + + } + aria-label='delete' + onClick={async () => { + await deletePost({ id }) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/components/EditDeleteSpaceButtons.tsx b/src/components/EditDeleteSpaceButtons.tsx new file mode 100644 index 0000000..4c60106 --- /dev/null +++ b/src/components/EditDeleteSpaceButtons.tsx @@ -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 +} + +export const EditDeleteSpaceButtons = (props: EditDeleteSpaceButtonsProps): JSX.Element => { + const { space } = props + const [, deleteSpace] = useDeleteSpaceMutation() + + return ( + <> + + } + aria-label='edit' + as={NoUnderlineLink} + /> + + } + aria-label='delete' + onClick={async () => { + await deleteSpace({ id: space.id }) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/components/EditPageAboutButton.tsx b/src/components/EditPageAboutButton.tsx new file mode 100644 index 0000000..6d32b5d --- /dev/null +++ b/src/components/EditPageAboutButton.tsx @@ -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 ( + <> + + + { + await updateInfo({ id: data?.page?.id, input: values }) + onEditingAboutClose() + }} + > + {({ isSubmitting }) => +
+ + + Update about + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/EditSpaceAboutButton.tsx b/src/components/EditSpaceAboutButton.tsx new file mode 100644 index 0000000..ac8b6d2 --- /dev/null +++ b/src/components/EditSpaceAboutButton.tsx @@ -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 ( + <> + + + { + await updateInfo({ id: data?.space?.id, input: values }) + onEditingAboutClose() + }} + > + {({ isSubmitting }) => +
+ + + Update about + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/EditSpaceRulesButton.tsx b/src/components/EditSpaceRulesButton.tsx new file mode 100644 index 0000000..9bf2cdd --- /dev/null +++ b/src/components/EditSpaceRulesButton.tsx @@ -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 ( + <> + + + { + await updateInfo({ id: data?.space?.id, input: values }) + onEditingRulesClose() + }} + > + {({ isSubmitting }) => +
+ + + Update rules + + + + + + + + Save + + + + + + } +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/EducationItem.tsx b/src/components/EducationItem.tsx new file mode 100644 index 0000000..e9dda2a --- /dev/null +++ b/src/components/EducationItem.tsx @@ -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[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 ( + + + education-item-photo + + {item.school} + {item?.status} + + { + item.startDate && + DateTime.fromISO(item.startDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT) + } + { + item.startDate && item.endDate && + + } + { + item.endDate && + DateTime.fromISO(item.endDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT) + } + + + { + data?.user?.id == me?.id && + + + + { + await updateEducationItem({ id: item.id, input: values }) + onEditingEducationItemClose() + }} + > + {({ isSubmitting }) => +
+ + + Update education + + + + + + + + + + + + + + + + + + + + Save + + + + + + } +
+
+ +
+ } +
+
+ ) +} \ No newline at end of file diff --git a/src/components/Experience.tsx b/src/components/Experience.tsx new file mode 100644 index 0000000..6f5a816 --- /dev/null +++ b/src/components/Experience.tsx @@ -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[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 ( + + + experience-photo + + {item.title} + {item?.workplace} + + { + item.startDate && + DateTime.fromISO(item.startDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT) + } + { + item.startDate && item.endDate && + + } + { + item.endDate && + DateTime.fromISO(item.endDate).setLocale('fr').toLocaleString(DateTime.DATE_SHORT) + } + + + { + data?.user?.id == me?.id && + + + + { + await updateExperience({ id: item.id, input: values }) + onEditingExperienceClose() + }} + > + {({ isSubmitting }) => +
+ + + Update experience + + + + + + + + + + + + + + + + + + + + Save + + + + + + } +
+
+ +
+ } +
+
+ ) +} \ No newline at end of file diff --git a/src/components/FileField.tsx b/src/components/FileField.tsx new file mode 100644 index 0000000..a819cd7 --- /dev/null +++ b/src/components/FileField.tsx @@ -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 ( + + {label} + { + values[`${name}`] ? + + {values[`${name}`].name} + { setValue(null) }} /> + + : + + { + setValue(acceptedFiles[0]) + }} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ + + { + isDragActive ? + 'Drop your file here ...' : + 'Drag \'n\' drop a file here, or click to select one' + } + +
+ )} +
+
+ } + + {error && {error}} +
+ ) +} \ No newline at end of file diff --git a/src/components/FollowPageButton.tsx b/src/components/FollowPageButton.tsx new file mode 100644 index 0000000..f019141 --- /dev/null +++ b/src/components/FollowPageButton.tsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/src/components/FollowUserButton.tsx b/src/components/FollowUserButton.tsx new file mode 100644 index 0000000..7a6181a --- /dev/null +++ b/src/components/FollowUserButton.tsx @@ -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 ( + + ) +} \ No newline at end of file diff --git a/src/components/FormSuccessMessage.tsx b/src/components/FormSuccessMessage.tsx new file mode 100644 index 0000000..1e0b4ec --- /dev/null +++ b/src/components/FormSuccessMessage.tsx @@ -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 ( + + + {message} + + ) +} \ No newline at end of file diff --git a/src/components/InboxItem.tsx b/src/components/InboxItem.tsx new file mode 100644 index 0000000..8aca96b --- /dev/null +++ b/src/components/InboxItem.tsx @@ -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 + 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 ( + { + 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) }} + > + + u/{inbox.partner.username} + { + showEllipsis && + + + } + _focusVisible={{ boxShadow: '0 0 0 3px #48BB78' }} + _hover={{ bgColor: 'gray.300' }} + _active={{ bgColor: 'gray.400' }} + /> + + + + + + + + + + Delete Conversation + + + Are you sure you want to delete this conversation? + + + + + + + + + + + + + } + + ) +} \ No newline at end of file diff --git a/src/components/IndexOffers.tsx b/src/components/IndexOffers.tsx new file mode 100644 index 0000000..111e9a7 --- /dev/null +++ b/src/components/IndexOffers.tsx @@ -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 + offersVariables: { limit: number, cursor: string | null } + setOffersVariables: Dispatch> +} + +export const IndexOffers = (props: IndexOffersProps): JSX.Element => { + const { offersData, offersFetching, user, offersVariables, setOffersVariables } = props + const checkPageOwnership = useCheckPageOwnership() + + return ( + <> + { + !offersData && offersFetching ? loading... : + + { + offersData?.offers?.offers?.length > 0 && + offersData.offers.offers.map(o => + o && + + + offer-photo + + + + {o.title} + + + + Posted by  + + + {o.creatorType == 'user' ? `u/${o.creator.username}` : `p/${o?.pageCreator.pageName}`} + + +  in  + + + s/{o.space.spaceName} + + + + {o.workplace} + {o.address} + {o.recruiting ? 'Recruiting' : 'Not recruiting'} + {DateTime.fromMillis(parseInt(o.createdAt)).toRelative()} + + { + ((user?.id && user.id == o?.creator?.id) || checkPageOwnership(o?.pageCreator?.id)) && + + + + } + + + ) + } + + } + { + offersData?.offers?.hasMore && + + + + } + + ) +} \ No newline at end of file diff --git a/src/components/IndexPosts.tsx b/src/components/IndexPosts.tsx new file mode 100644 index 0000000..1dc9bfd --- /dev/null +++ b/src/components/IndexPosts.tsx @@ -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 + postsVariables: { limit: number, cursor: string | null } + setPostsVariables: Dispatch> +} + +export const IndexPosts = (props: IndexPostsProps): JSX.Element => { + const { postsData, postsFetching, user, postsVariables, setPostsVariables } = props + const checkPageOwnership = useCheckPageOwnership() + + return ( + <> + { + !postsData || postsFetching ? loading... : + + { + postsData?.posts?.posts?.length > 0 && + postsData.posts.posts.map(p => + p && + + {/* clone to avoid side effect of up/downvoting on video */} + + + + Posted by  + + + {p.creatorType == 'user' ? `u/${p.creator.username}` : `p/${p?.pageCreator.pageName}`} + + +  in  + + + s/{p?.space?.spaceName} + + + + + + {p.title} + + + + { + p.tags?.map((t, index) => { + const tagStyling = tagStylingMap[`${t.name}`] + return ( + t && + + + { + tagStyling.icon && + + tagStyling.icon} /> + + } + {t.name} + + + ) + }) + } + + + + + + { + ((user?.id && user.id == p?.creator?.id) || checkPageOwnership(p?.pageCreator?.id)) && + + + + } + + + ) + } + + } + { + postsData?.posts?.hasMore && + + + + } + + ) +} \ No newline at end of file diff --git a/src/components/InputField.tsx b/src/components/InputField.tsx new file mode 100644 index 0000000..6cbfd18 --- /dev/null +++ b/src/components/InputField.tsx @@ -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 & PropsOf & + TextareaHTMLAttributes & PropsOf & + PropsOf + ) & { + label: string + name: string + inputType?: 'input' | 'textarea' | 'quill' + onQuillChangeEffect?: () => void + } + +type InputComponentProps = InputHTMLAttributes & { + label: string + name: string + size?: any +} + +type TextAreaComponentProps = TextareaHTMLAttributes & { + label: string + name: string + size?: any +} + +type QuillComponentProps = PropsOf & { + label: string + name: string + onQuillChangeEffect?: () => void +} + +const InputComponent = (props: InputComponentProps): JSX.Element => { + const { label, name, ...rest } = props + const [field, { error }] = useField(props) + return ( + + {label} + + {error && {error}} + + ) +} + +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 ( + + {label} +