---
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
import { LogoutDocument, MeDocument, PostsDocument } from '@/generated/graphql/graphql'
|
||||
import { useMutation, useQuery } from '@apollo/client/react'
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
|
||||
const NavBar: React.FC = () => {
|
||||
const { data, loading, refetch } = useQuery(MeDocument)
|
||||
const { refetch: refetchPosts } = useQuery(PostsDocument)
|
||||
const [logout] = useMutation(LogoutDocument)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<Flex bgColor='tan' paddingY={4} paddingX={6} ml='auto' minH='72px' alignItems='center'>
|
||||
<Box mr='auto'>
|
||||
<Link href='/'>
|
||||
<Button>Home</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box ml='auto'>
|
||||
{
|
||||
loading &&
|
||||
<Text>Loading...</Text>
|
||||
}
|
||||
{
|
||||
!loading && !data?.me &&
|
||||
<>
|
||||
<Link href='/login' mr={4}>
|
||||
<Button>Login</Button>
|
||||
</Link>
|
||||
<Link href='/register' mr={4}>
|
||||
<Button>Register</Button>
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
{
|
||||
data?.me &&
|
||||
<Flex alignItems='center'>
|
||||
<Text mr={4}>Logged in as {data.me.username}!</Text>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
await refetch() // Calling refetch will also refetch the data for any other components using useQuery(MeDocument)
|
||||
await refetchPosts()
|
||||
if (pathname == '/') {
|
||||
router.refresh()
|
||||
}
|
||||
else {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default NavBar
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
import { InputField, TextareaField, Wrapper } from '@/components'
|
||||
import { CreatePostDocument, PostInput, PostsDocument } from '@/generated/graphql/graphql'
|
||||
import { useAuthenticate } from '@/hooks'
|
||||
import { errorMapper } from '@/utils'
|
||||
import { useMutation, useQuery } from '@apollo/client/react'
|
||||
import { Box, Button, Flex } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
|
||||
const CreatePostPage: React.FC = () => {
|
||||
useAuthenticate()
|
||||
const router = useRouter()
|
||||
const [createPost] = useMutation(CreatePostDocument)
|
||||
const { refetch } = useQuery(PostsDocument)
|
||||
|
||||
return (
|
||||
<Wrapper variant='small'>
|
||||
<Formik
|
||||
initialValues={{ title: '', content: '' } as PostInput}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await createPost({ variables: { input: values } })
|
||||
const errors = response.data?.createPost.errors
|
||||
if (errors) {
|
||||
setErrors(errorMapper(errors))
|
||||
}
|
||||
else if (response.data?.createPost.post) {
|
||||
refetch()
|
||||
router.push(`/post/${response.data.createPost.post.id}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<Flex flexDir='column' justifyContent='center'>
|
||||
<InputField name='title' label='Title' placeholder='title' />
|
||||
<Box mt={4}>
|
||||
<TextareaField name='content' label='Content' placeholder='content' />
|
||||
</Box>
|
||||
<Button type='submit' colorScheme='teal' mt={4} isLoading={isSubmitting} alignSelf='center'>Create</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
export default CreatePostPage
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import { FormSuccessMessage, InputField, TextareaField, Wrapper } from '@/components'
|
||||
import { PostDocument, PostInput, UpdatePostDocument } from '@/generated/graphql/graphql'
|
||||
import { useAuthenticate } from '@/hooks'
|
||||
import { errorMapper } from '@/utils'
|
||||
import { useMutation, useQuery } from '@apollo/client/react'
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { use, useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
|
||||
|
||||
const EditPostPage: React.FC<Props> = ({ params }) => {
|
||||
const { id } = use(params)
|
||||
useAuthenticate()
|
||||
const { data, loading, refetch } = useQuery(PostDocument, { variables: { id } })
|
||||
const [updatePost] = useMutation(UpdatePostDocument)
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
|
||||
|
||||
if (loading) {
|
||||
return <Text>Loading...</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper variant='small'>
|
||||
<Formik
|
||||
initialValues={{ title: data?.post?.title || '', content: data?.post?.content || '' } as PostInput}
|
||||
onSubmit={async ({ title, content }, { setErrors }) => {
|
||||
const response = await updatePost({ variables: { id, title, content } })
|
||||
const errors = response.data?.updatePost.errors
|
||||
if (errors) {
|
||||
setErrors(errorMapper(errors))
|
||||
}
|
||||
else if (response.data?.updatePost.post) {
|
||||
refetch()
|
||||
setShowSuccessMessage(true)
|
||||
setTimeout(() => {
|
||||
setShowSuccessMessage(false)
|
||||
}, 10000)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<Flex flexDir='column' justifyContent='center'>
|
||||
<InputField name='title' label='Title' placeholder='title' />
|
||||
<Box mt={4}>
|
||||
<TextareaField name='content' label='Content' placeholder='content' />
|
||||
</Box>
|
||||
{showSuccessMessage && <FormSuccessMessage message='Post successfully updated!' />}
|
||||
<Button type='submit' colorScheme='teal' mt={4} isLoading={isSubmitting} alignSelf='center'>Update</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditPostPage
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import { FormErrorMessage, FormSuccessMessage, InputField, Wrapper } from '@/components'
|
||||
import { ForgotPasswordDocument } from '@/generated/graphql/graphql'
|
||||
import { errorMapper } from '@/utils'
|
||||
import { useMutation } from '@apollo/client/react'
|
||||
import { Button } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useState } from 'react'
|
||||
|
||||
const ForgotPasswordPage: React.FC = () => {
|
||||
const [forgotPassword] = useMutation(ForgotPasswordDocument)
|
||||
const [message, setMessage] = useState('')
|
||||
const [messageType, setMessageType] = useState<'success' | 'error' | ''>('')
|
||||
|
||||
return (
|
||||
<Wrapper variant='small'>
|
||||
<Formik
|
||||
initialValues={{ email: '' }}
|
||||
onSubmit={async ({ email }, { setErrors }) => {
|
||||
setMessage('')
|
||||
setMessageType('')
|
||||
const response = await forgotPassword({ variables: { email } })
|
||||
const errors = response.data?.forgotPassword.errors
|
||||
if (errors) {
|
||||
setErrors(errorMapper(errors))
|
||||
}
|
||||
else if (response.data?.forgotPassword.message) {
|
||||
setMessage(response.data.forgotPassword.message)
|
||||
setMessageType(response.data.forgotPassword.messageType as any)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<InputField name='email' label="Enter your account's email to reset your password:" placeholder='email' />
|
||||
{
|
||||
(message && messageType == 'success') &&
|
||||
<FormSuccessMessage message={message}/>
|
||||
}
|
||||
{
|
||||
(message && messageType == 'error') &&
|
||||
<FormErrorMessage message={message}/>
|
||||
}
|
||||
<Button type='submit' colorScheme='teal' mt={4} isLoading={isSubmitting}>Submit</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordPage
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Providers } from '@/Providers'
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import NavBar from './NavBar'
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'WreckIt ⚒️'
|
||||
}
|
||||
|
||||
export const runtime = process.env.NODE_ENV == 'production' ? 'edge' : 'nodejs'
|
||||
|
||||
const RootLayout = ({ children, }: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>
|
||||
<Providers>
|
||||
<NavBar />
|
||||
<Flex justifyContent='center' width='100%' padding={4} minH='calc(100vh - 72px)'>
|
||||
{children}
|
||||
</Flex>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
export default RootLayout
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
import { InputField, Wrapper } from '@/components'
|
||||
import { LoginDocument, MeDocument, PostsDocument } from '@/generated/graphql/graphql'
|
||||
import { errorMapper } from '@/utils'
|
||||
import { useMutation, useQuery } from '@apollo/client/react'
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import { Box, Button } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { refetch } = useQuery(MeDocument)
|
||||
const { refetch: refetchPosts } = useQuery(PostsDocument)
|
||||
const [login] = useMutation(LoginDocument)
|
||||
|
||||
return (
|
||||
<Wrapper variant='small'>
|
||||
<Formik
|
||||
initialValues={{ username: '', password: '' }}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await login({ variables: { input: values } })
|
||||
const errors = response.data?.login.errors
|
||||
if (errors) {
|
||||
setErrors(errorMapper(errors))
|
||||
}
|
||||
else if (response.data?.login.user) {
|
||||
// Successful login
|
||||
await refetch() // Refetch client-side
|
||||
await refetchPosts()
|
||||
const destination = searchParams.get('redirect')
|
||||
if (destination) {
|
||||
router.push(destination)
|
||||
}
|
||||
else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<InputField name='username' label='Username' placeholder='username' />
|
||||
<Box mt={4}>
|
||||
<InputField name='password' label='Password' placeholder='password' type='password' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Link href='/forgot-password'>Forgot password?</Link>
|
||||
</Box>
|
||||
<Button type='submit' colorScheme='teal' mt={4} isLoading={isSubmitting}>Login</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<Suspense>
|
||||
<Page />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
import { Post, Wrapper } from '@/components'
|
||||
import { PostsDocument } from '@/generated/graphql/graphql'
|
||||
import { useQuery } from '@apollo/client/react'
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import { Box, Button, Flex, Heading, Stack, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
|
||||
const Home = (): React.ReactNode => {
|
||||
// https://www.apollographql.com/docs/react/pagination/core-api/
|
||||
const { data, loading, fetchMore } = useQuery(PostsDocument)
|
||||
const [doesntHaveMore, setDoesntHaveMore] = useState(false)
|
||||
|
||||
return (
|
||||
<Box w='100%'>
|
||||
<Wrapper>
|
||||
<Flex w='100%'>
|
||||
<Heading as='h3' size='lg'>WreckIt ⚒️</Heading>
|
||||
<Link href='/create-post' ml='auto'>
|
||||
<Button>Create Post</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
<Flex flexDir='column' alignItems='center'>
|
||||
{
|
||||
loading && !data?.posts &&
|
||||
<Text>Loading...</Text>
|
||||
}
|
||||
<Stack spacing={4} mt={8} w='100%'>
|
||||
{
|
||||
data?.posts.map((p, idx) => <Post post={p} key={idx} />)
|
||||
}
|
||||
</Stack>
|
||||
|
||||
<Flex w='100%' mt={4}>
|
||||
{
|
||||
(data?.posts?.length && !doesntHaveMore) ?
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const result = await fetchMore({ variables: { cursor: data.posts[data.posts.length - 1].createdAt } })
|
||||
if (result.data?.posts.length == 0) {
|
||||
setDoesntHaveMore(true)
|
||||
}
|
||||
}}
|
||||
m='auto'
|
||||
>
|
||||
More
|
||||
</Button>
|
||||
: <></>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Wrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
import { DeletePostDocument, PostQuery } from '@/generated/graphql/graphql'
|
||||
import { useMutation } from '@apollo/client/react'
|
||||
import { DeleteIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button, Flex, IconButton, useDisclosure } from '@chakra-ui/react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useRef } from 'react'
|
||||
|
||||
interface Props {
|
||||
data: PostQuery
|
||||
}
|
||||
|
||||
export const ClientSection: React.FC<Props> = ({ data }) => {
|
||||
const router = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const cancelRef: any = useRef(null)
|
||||
const [deletePost] = useMutation(DeletePostDocument)
|
||||
|
||||
return (
|
||||
<Flex mr='auto' alignItems='center' mt={4}>
|
||||
<Link href={`/edit-post/${data?.post?.id}`}>
|
||||
<IconButton
|
||||
aria-label='edit-button'
|
||||
icon={<EditIcon />}
|
||||
mr={4}
|
||||
/>
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label='delete-button'
|
||||
icon={<DeleteIcon />}
|
||||
colorScheme='red'
|
||||
onClick={onOpen}
|
||||
/>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||
Delete Post
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
Are you sure? You can't undo this action afterwards.
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme='red'
|
||||
onClick={async () => {
|
||||
await deletePost({
|
||||
variables: { id: data.post?.id! },
|
||||
// https://stackoverflow.com/questions/63192774/apollo-client-delete-item-from-cache
|
||||
// https://www.apollographql.com/docs/react/caching/garbage-collection/#cacheevict
|
||||
update: cache => {
|
||||
const normalizedId = cache.identify({ id: data.post?.id!, __typename: 'Post' })
|
||||
cache.evict({ id: normalizedId })
|
||||
cache.gc()
|
||||
}
|
||||
})
|
||||
router.refresh()
|
||||
}}
|
||||
ml={3}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
return (
|
||||
<Text>Loading...</Text>
|
||||
)
|
||||
}
|
||||
export default Loading
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Wrapper } from '@/components'
|
||||
import { SESSION_COOKIE_NAME } from '@/constants'
|
||||
import { MeDocument, PostDocument } from '@/generated/graphql/graphql'
|
||||
import { createApolloClient } from '@/lib'
|
||||
import { Heading, Text } from '@chakra-ui/react'
|
||||
import { cookies } from 'next/headers'
|
||||
import { ClientSection } from './ClientSection'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{
|
||||
id: string
|
||||
}>
|
||||
}
|
||||
|
||||
const PostPage: React.FC<Props> = async ({ params }) => {
|
||||
const { id } = await params
|
||||
const cookieStore = await cookies()
|
||||
const cookie = cookieStore.get(SESSION_COOKIE_NAME)?.value
|
||||
const apollo = await createApolloClient(cookie)
|
||||
const { data } = await apollo.query({ query: PostDocument, variables: { id } })
|
||||
const { data: meData } = await apollo.query({ query: MeDocument })
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{
|
||||
data?.post ?
|
||||
<>
|
||||
<Heading as='h3' size='md'>{data.post.title}</Heading>
|
||||
<Text>Posted by {data.post.author.username}</Text>
|
||||
<Text mt={4}>{data.post.content}</Text>
|
||||
{
|
||||
data?.post?.authorID == meData?.me?.id &&
|
||||
<ClientSection data={data} />
|
||||
}
|
||||
</>
|
||||
:
|
||||
<>Post not found!</>
|
||||
}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
export default PostPage
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import { InputField, Wrapper } from '@/components'
|
||||
import { MeDocument, RegisterDocument } from '@/generated/graphql/graphql'
|
||||
import { errorMapper } from '@/utils'
|
||||
import { useMutation, useQuery } from '@apollo/client/react'
|
||||
import { Box, Button } from '@chakra-ui/react'
|
||||
import { Form, Formik } from 'formik'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
|
||||
const RegisterPage: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { refetch } = useQuery(MeDocument)
|
||||
const [register] = useMutation(RegisterDocument)
|
||||
return (
|
||||
<Wrapper variant='small'>
|
||||
<Formik
|
||||
initialValues={{ username: '', password: '' }}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const response = await register({ variables: { input: values } })
|
||||
const errors = response.data?.register.errors
|
||||
if (errors) {
|
||||
setErrors(errorMapper(errors))
|
||||
}
|
||||
else if (response.data?.register.user) {
|
||||
// Successful register
|
||||
await refetch()
|
||||
router.push('/')
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<InputField name='email' label='Email' placeholder='email' />
|
||||
<Box mt={4}>
|
||||
<InputField name='username' label='Username' placeholder='username' />
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<InputField name='password' label='Password' placeholder='password' type='password' />
|
||||
</Box>
|
||||
<Button type='submit' colorScheme='teal' mt={4} isLoading={isSubmitting}>Register</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
export default RegisterPage
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PasswordResetForm } from '@/components'
|
||||
import { SESSION_COOKIE_NAME } from '@/constants'
|
||||
import { CheckResetPasswordTokenDocument } from '@/generated/graphql/graphql'
|
||||
import { createApolloClient } from '@/lib'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
interface Props {
|
||||
params: Promise<{
|
||||
token: string
|
||||
}>
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const ResetPasswordPage: React.FC<Props> = async ({ params }) => {
|
||||
const { token } = await params
|
||||
const cookieStore = await cookies()
|
||||
const cookie = cookieStore.get(SESSION_COOKIE_NAME)?.value
|
||||
const apollo = await createApolloClient(cookie)
|
||||
const { data, error } = await apollo.query({ query: CheckResetPasswordTokenDocument, variables: { token } })
|
||||
|
||||
if (error || data === undefined) {
|
||||
console.log(error)
|
||||
return <Text>An error has occured. Please try again later.</Text>
|
||||
}
|
||||
|
||||
else if (!data.checkResetPasswordToken) {
|
||||
return <Text>Invalid token!</Text>
|
||||
}
|
||||
|
||||
else {
|
||||
return <PasswordResetForm token={token}/>
|
||||
}
|
||||
}
|
||||
export default ResetPasswordPage
|
||||
Reference in New Issue
Block a user