mirror of
https://github.com/hexolan/panels.git
synced 2026-03-27 04:44:10 +00:00
init frontend
This commit is contained in:
45
services/frontend/src/pages/Error.tsx
Normal file
45
services/frontend/src/pages/Error.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Link, useRouteError, isRouteErrorResponse } from 'react-router-dom'
|
||||
import { Title, Text, Button, Center, Container, Group, rem } from '@mantine/core'
|
||||
|
||||
const ErrorPage = () => {
|
||||
const error = useRouteError()
|
||||
|
||||
let title = 'Uh, oh!'
|
||||
let subTitle = 'Something went wrong.'
|
||||
if (isRouteErrorResponse(error)) {
|
||||
title = `Error ${error.status}`
|
||||
subTitle = error.statusText
|
||||
} else if (error instanceof Error) {
|
||||
subTitle = error.message
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='100%'>
|
||||
<Container>
|
||||
<Title
|
||||
align='center'
|
||||
weight={800}
|
||||
sx={(theme) => ({
|
||||
fontSize: rem(38),
|
||||
[theme.fn.smallerThan('sm')]: {
|
||||
fontSize: rem(32),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
<Text size='lg' color='dimmed' maw={rem(250)} align='center' my='xl'>
|
||||
{subTitle}
|
||||
</Text>
|
||||
|
||||
<Group position='center'>
|
||||
<Button component={Link} to='/' variant='subtle' size='md'>
|
||||
Back to Home
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorPage
|
||||
47
services/frontend/src/pages/ExplorePanels.tsx
Normal file
47
services/frontend/src/pages/ExplorePanels.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { IconMessages, IconTableOff } from '@tabler/icons-react'
|
||||
import { Container, Stack, Paper, Box, Title, Text, Anchor, Divider, ThemeIcon, Group } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
|
||||
const ExplorePanelsPage = () => {
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
|
||||
return (
|
||||
<Container mt='xl'>
|
||||
<Title>Explore Panels</Title>
|
||||
{currentUser && (
|
||||
<Text color='dimmed' size='sm' mt={5}>
|
||||
Alternatively you could <Anchor size='sm' component={Link} to='/panels/new'>create your own.</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
<Divider my='md' variant='dotted' />
|
||||
|
||||
<Stack spacing='sm' align='stretch'>
|
||||
<Paper shadow='xl' radius='md' p='md' withBorder component={Link} to='/panel/Panel'>
|
||||
<Group>
|
||||
<ThemeIcon color='teal' variant='light' size='xl'><IconMessages /></ThemeIcon>
|
||||
<Box>
|
||||
<Text weight={600}>Panel</Text>
|
||||
<Text>The first and therefore defacto primary panel.</Text>
|
||||
<Text color='dimmed' size='xs' mt={3}>Click to View</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper shadow='xl' radius='md' p='md' withBorder component='a' href='https://github.com/hexolan/Panels/'>
|
||||
<Group>
|
||||
<ThemeIcon color='red' variant='light' size='xl'><IconTableOff /></ThemeIcon>
|
||||
<Box>
|
||||
<Text weight={600}>Note</Text>
|
||||
<Text>This page is exemplary as this feature is currently unimplemented.</Text>
|
||||
<Text color='dimmed' size='xs' mt={3}>Planned Functionality</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExplorePanelsPage
|
||||
17
services/frontend/src/pages/Home.tsx
Normal file
17
services/frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Container, Title, Stack, Divider } from '@mantine/core'
|
||||
|
||||
import HomePostFeed from '../components/HomePostFeed'
|
||||
|
||||
const Homepage = () => {
|
||||
return (
|
||||
<Container mt='xl'>
|
||||
<Title>Feed</Title>
|
||||
<Divider my='md' variant='dotted' />
|
||||
<Stack my='lg' spacing='md'>
|
||||
<HomePostFeed />
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Homepage
|
||||
77
services/frontend/src/pages/NewPanel.tsx
Normal file
77
services/frontend/src/pages/NewPanel.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm, hasLength } from '@mantine/form'
|
||||
import { Center, Container, Paper, Title, Text, TextInput, Textarea, Stack, Button } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useCreatePanelMutation } from '../app/api/panels'
|
||||
import type { CreatePanelData } from '../app/types/panels'
|
||||
|
||||
const NewPanelPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
// Ensure the user is authenticated
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (currentUser === null) {
|
||||
throw Error('Authentication Required')
|
||||
}
|
||||
|
||||
const panelForm = useForm<CreatePanelData>({
|
||||
initialValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({ min: 3, max: 20 }, 'Panel name must be between 3 and 20 characters long'),
|
||||
description: hasLength({ min: 3, max: 512 }, 'Description must be between 3 and 512 characters'),
|
||||
}
|
||||
})
|
||||
|
||||
const [createPanel, { isLoading }] = useCreatePanelMutation()
|
||||
const submitPanelForm = async (values: CreatePanelData) => {
|
||||
await createPanel({
|
||||
...values
|
||||
}).unwrap().then((panel) => {
|
||||
navigate(`/panel/${panel.name}`)
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='95%'>
|
||||
<Container>
|
||||
<Title align='center' weight={900}>Create a Panel</Title>
|
||||
|
||||
<Paper withBorder shadow='md' radius='md' p={30} mt={30}>
|
||||
<form onSubmit={panelForm.onSubmit(submitPanelForm)}>
|
||||
<Stack spacing='md'>
|
||||
<TextInput
|
||||
label='Name'
|
||||
placeholder='e.g. music, programming, football'
|
||||
{...panelForm.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='Description'
|
||||
placeholder='e.g. The place to talk about all things music related...'
|
||||
{...panelForm.getInputProps('description')}
|
||||
/>
|
||||
|
||||
{ errorMsg && <Text color='red' align='center'>{'Error: ' + errorMsg}</Text> }
|
||||
|
||||
<Button type='submit' variant='outline' color='teal' disabled={isLoading} fullWidth>Create Panel</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPanelPage
|
||||
76
services/frontend/src/pages/NewPanelPost.tsx
Normal file
76
services/frontend/src/pages/NewPanelPost.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react'
|
||||
import { useOutletContext, useNavigate } from 'react-router-dom'
|
||||
import { useForm, hasLength } from '@mantine/form'
|
||||
import { Stack, Paper, Text, TextInput, Textarea, Button } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useCreatePanelPostMutation } from '../app/api/posts'
|
||||
import type { CreatePostData } from '../app/types/posts'
|
||||
import type { PanelContext } from '../components/PanelLayout'
|
||||
|
||||
const NewPanelPostPage = () => {
|
||||
const { panel } = useOutletContext<PanelContext>()
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Ensure the user is authenticated
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (currentUser === null) {
|
||||
throw Error('You must be authenticated to create posts')
|
||||
}
|
||||
|
||||
const createPostForm = useForm<CreatePostData>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
content: '',
|
||||
},
|
||||
validate: {
|
||||
title: hasLength({ min: 3, max: 512 }, 'Title must be between 3 and 512 characters'),
|
||||
content: hasLength({ min: 3, max: 2048 }, 'Content must be between 3 and 2048 characters'),
|
||||
}
|
||||
})
|
||||
|
||||
const [createPost, { isLoading }] = useCreatePanelPostMutation()
|
||||
const submitPost = async (values: CreatePostData) => {
|
||||
await createPost({
|
||||
panelId: panel.id,
|
||||
data: values
|
||||
}).unwrap().then((post) => {
|
||||
navigate(`/panel/${panel.name}/post/${post.id}`)
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper shadow='md' radius='md' p='lg' withBorder>
|
||||
<form onSubmit={createPostForm.onSubmit(submitPost)}>
|
||||
<Stack spacing='md'>
|
||||
<TextInput
|
||||
label='Title'
|
||||
placeholder='Post Title'
|
||||
{...createPostForm.getInputProps('title')}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label='Content'
|
||||
placeholder='Post Content'
|
||||
{...createPostForm.getInputProps('content')}
|
||||
/>
|
||||
|
||||
{ errorMsg && <Text color='red' align='center'>{'Error: ' + errorMsg}</Text> }
|
||||
|
||||
<Button type='submit' variant='outline' color='teal' disabled={isLoading} fullWidth>
|
||||
Create Post
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPanelPostPage
|
||||
17
services/frontend/src/pages/Panel.tsx
Normal file
17
services/frontend/src/pages/Panel.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { Stack } from '@mantine/core'
|
||||
|
||||
import PanelPostFeed from '../components/PanelPostFeed'
|
||||
import type { PanelContext } from '../components/PanelLayout'
|
||||
|
||||
function PanelPage() {
|
||||
const { panel } = useOutletContext<PanelContext>()
|
||||
|
||||
return (
|
||||
<Stack my='lg' spacing='md'>
|
||||
<PanelPostFeed panel={panel} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default PanelPage
|
||||
65
services/frontend/src/pages/PanelPost.tsx
Normal file
65
services/frontend/src/pages/PanelPost.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { Box, Stack, Divider } from '@mantine/core'
|
||||
|
||||
import PagePost from '../components/PagePost'
|
||||
import CommentsFeed from '../components/CommentsFeed'
|
||||
import PostCommentsFeed from '../components/PostCommentsFeed'
|
||||
import CreateComment from '../components/CreateComment'
|
||||
import LoadingBar from '../components/LoadingBar'
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useGetPanelPostQuery } from '../app/api/posts'
|
||||
import type { PanelContext } from '../components/PanelLayout'
|
||||
import type { ErrorResponse } from '../app/types/api'
|
||||
import type { Comment } from '../app/types/common'
|
||||
|
||||
type PanelPostPageParams = {
|
||||
panelName: string;
|
||||
postId: string;
|
||||
}
|
||||
|
||||
function PanelPostPage() {
|
||||
const { panel } = useOutletContext<PanelContext>()
|
||||
const { postId } = useParams<PanelPostPageParams>();
|
||||
if (postId === undefined) { throw Error('post id not provided') }
|
||||
|
||||
const [newComments, setNewComments] = useState<Comment[]>([])
|
||||
const addNewComment = (comment: Comment) => {
|
||||
setNewComments([comment].concat(newComments))
|
||||
}
|
||||
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
|
||||
// Fetch the post
|
||||
const { data, error, isLoading } = useGetPanelPostQuery({ panelId: panel.id, id: postId })
|
||||
if (isLoading) {
|
||||
return <LoadingBar />;
|
||||
} else if (!data) {
|
||||
if (!error) {
|
||||
throw Error('Unknown error occured')
|
||||
} else if ('data' in error) {
|
||||
const errResponse = error.data as ErrorResponse
|
||||
if (errResponse.msg) {
|
||||
throw Error(errResponse.msg)
|
||||
} else {
|
||||
throw Error('Unexpected API error occured')
|
||||
}
|
||||
} else {
|
||||
throw Error('Failed to access the API')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb='lg'>
|
||||
<PagePost post={data} />
|
||||
<Divider my='md' variant='none' />
|
||||
<Stack spacing='sm'>
|
||||
{ currentUser && <CreateComment post={data} addNewComment={addNewComment} /> }
|
||||
{ newComments.length > 0 && <CommentsFeed comments={newComments} /> }
|
||||
<PostCommentsFeed post={data} />
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PanelPostPage
|
||||
109
services/frontend/src/pages/PanelSettings.tsx
Normal file
109
services/frontend/src/pages/PanelSettings.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { useForm, hasLength } from '@mantine/form'
|
||||
import { Paper, Center, Stack, Group, Text, TextInput, Textarea, Button } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useDeletePanelByIdMutation, useUpdatePanelByIdMutation } from '../app/api/panels'
|
||||
import type { Panel } from '../app/types/common'
|
||||
import type { UpdatePanelData } from '../app/types/panels'
|
||||
import type { PanelContext } from '../components/PanelLayout'
|
||||
|
||||
const UpdatePanelForm = ({
|
||||
panel,
|
||||
setPanel,
|
||||
setModifying,
|
||||
setErrorMsg
|
||||
}: {
|
||||
panel: Panel,
|
||||
setPanel: React.Dispatch<Panel>,
|
||||
setModifying: React.Dispatch<boolean>,
|
||||
setErrorMsg: React.Dispatch<string>
|
||||
}) => {
|
||||
const panelForm = useForm<UpdatePanelData>({
|
||||
initialValues: {
|
||||
name: panel.name,
|
||||
description: panel.description,
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({ min: 3, max: 20 }, 'Name must be between 3 and 20 characters long'),
|
||||
description: hasLength({ min: 3, max: 512 }, 'Description must be between 3 and 512 characters'),
|
||||
}
|
||||
})
|
||||
|
||||
const [updatePanel, { isLoading }] = useUpdatePanelByIdMutation()
|
||||
const submitUpdatePanel = async (values: UpdatePanelData) => {
|
||||
await updatePanel({
|
||||
id: panel.id,
|
||||
data: values
|
||||
}).unwrap().then((panelInfo) => {
|
||||
setErrorMsg('')
|
||||
setModifying(false)
|
||||
setPanel(panelInfo)
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={panelForm.onSubmit(submitUpdatePanel)}>
|
||||
<Stack spacing='md'>
|
||||
<TextInput label='Name' {...panelForm.getInputProps('name')} />
|
||||
<Textarea label='Description' {...panelForm.getInputProps('description')} />
|
||||
|
||||
<Button type='submit' variant='outline' color='teal' disabled={isLoading} fullWidth>Update</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelSettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
const [errorMsg, setErrorMsg] = useState<string>('')
|
||||
const [modifying, setModifying] = useState<boolean>(false)
|
||||
const { panel, setPanel } = useOutletContext<PanelContext>()
|
||||
|
||||
// Check permissions
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (!currentUser || !currentUser.isAdmin) {
|
||||
throw new Error('You do not have permission to view that page.')
|
||||
}
|
||||
|
||||
// Panel Deletion
|
||||
const [deletePanel, { isLoading: isLoadingDelPanel }] = useDeletePanelByIdMutation()
|
||||
const submitDeletePanel = async () => {
|
||||
await deletePanel({ id: panel.id }).unwrap().then(() => {
|
||||
navigate('/')
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper mt='md' radius='lg' shadow='md' p='lg' withBorder>
|
||||
<Center>
|
||||
<Group spacing='sm'>
|
||||
{modifying
|
||||
? <Button color='teal' onClick={() => { setModifying(false); setErrorMsg('') }}>Stop Modifying</Button>
|
||||
: <Button color='teal' onClick={() => setModifying(true)}>Modify Panel</Button>
|
||||
}
|
||||
<Button color='red' onClick={() => submitDeletePanel()} disabled={isLoadingDelPanel}>Delete Panel</Button>
|
||||
</Group>
|
||||
</Center>
|
||||
|
||||
{modifying && <UpdatePanelForm panel={panel} setPanel={setPanel} setModifying={setModifying} setErrorMsg={setErrorMsg} />}
|
||||
|
||||
{errorMsg && <Text color='red' mt='sm'>{'Error: ' + errorMsg}</Text>}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default PanelSettingsPage
|
||||
83
services/frontend/src/pages/SignIn.tsx
Normal file
83
services/frontend/src/pages/SignIn.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useForm, hasLength } from '@mantine/form'
|
||||
import { Center, Container, Paper, Title, Text, Anchor, TextInput, PasswordInput, Stack, Button } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useLoginMutation } from '../app/api/auth'
|
||||
import type { LoginRequest } from '../app/types/auth'
|
||||
|
||||
function SignInPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Ensure the user isn't authenticated already
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (currentUser) {
|
||||
throw new Error('You are already authenticated.')
|
||||
}
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const loginForm = useForm<LoginRequest>({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 3, max: 32 }, 'Invalid username'),
|
||||
password: hasLength({ min: 8 }, 'Invalid password'),
|
||||
}
|
||||
})
|
||||
|
||||
const [login, { isLoading }] = useLoginMutation()
|
||||
const submitLoginForm = async (values: LoginRequest) => {
|
||||
// Attempt to authenticate the user.
|
||||
await login(values).unwrap().then(() => {
|
||||
// Redirect to homepage.
|
||||
navigate('/')
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='95%'>
|
||||
<Container>
|
||||
<Title align='center' weight={900}>Sign In</Title>
|
||||
<Text color='dimmed' size='sm' align='center' mt={5}>
|
||||
Do not have an account yet?{' '}
|
||||
<Anchor size='sm' component={Link} to='/signup'>Sign up</Anchor> instead.
|
||||
</Text>
|
||||
|
||||
|
||||
<Paper withBorder shadow='md' radius='md' p={30} mt={30}>
|
||||
<form onSubmit={loginForm.onSubmit(submitLoginForm)}>
|
||||
<Stack spacing='md'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
placeholder='Your username'
|
||||
{...loginForm.getInputProps('username')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
placeholder='Your password'
|
||||
{...loginForm.getInputProps('password')}
|
||||
/>
|
||||
|
||||
{ errorMsg && <Text color='red' align='center'>{'Error: ' + errorMsg}</Text> }
|
||||
|
||||
<Button type='submit' color='teal' disabled={isLoading} fullWidth>Login</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignInPage
|
||||
97
services/frontend/src/pages/SignUp.tsx
Normal file
97
services/frontend/src/pages/SignUp.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useForm, hasLength, matchesField } from '@mantine/form'
|
||||
import { Center, Container, Paper, Title, Text, Anchor, TextInput, PasswordInput, Stack, Button } from '@mantine/core'
|
||||
|
||||
import { useAppSelector } from '../app/hooks'
|
||||
import { useRegisterUserMutation } from '../app/api/users'
|
||||
|
||||
type RegistrationFormValues = {
|
||||
username: string;
|
||||
password: string;
|
||||
confPassword: string;
|
||||
}
|
||||
|
||||
const SignUpPage = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Ensure the user isn't authenticated already
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (currentUser) {
|
||||
throw new Error('You are already authenticated.')
|
||||
}
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const registrationForm = useForm<RegistrationFormValues>({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
confPassword: '',
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 3, max: 32 }, 'Username must be between 3 and 32 characters'),
|
||||
password: hasLength({ min: 8 }, 'Password must have a minimum of 8 characters'),
|
||||
confPassword: matchesField('password', 'Confirmation password does not match'),
|
||||
}
|
||||
})
|
||||
|
||||
const [registerUser, { isLoading }] = useRegisterUserMutation()
|
||||
const submitRegistrationForm = async (values: RegistrationFormValues) => {
|
||||
await registerUser({
|
||||
username: values.username,
|
||||
password: values.password
|
||||
}).unwrap().then(() => {
|
||||
// Redirect to homepage.
|
||||
navigate('/')
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='95%'>
|
||||
<Container>
|
||||
<Title align='center' weight={900}>Sign Up</Title>
|
||||
<Text color='dimmed' size='sm' align='center' mt={5}>
|
||||
Already have an account?{' '}
|
||||
<Anchor size='sm' component={Link} to='/signin'>Sign in</Anchor> instead.
|
||||
</Text>
|
||||
|
||||
<Paper withBorder shadow='md' radius='md' p={30} mt={30}>
|
||||
<form onSubmit={registrationForm.onSubmit(submitRegistrationForm)}>
|
||||
<Stack spacing='md'>
|
||||
<TextInput
|
||||
label='Username'
|
||||
placeholder='Your username'
|
||||
{...registrationForm.getInputProps('username')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
placeholder='Your password'
|
||||
{...registrationForm.getInputProps('password')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label='Confirm Password'
|
||||
placeholder='Confirm password'
|
||||
{...registrationForm.getInputProps('confPassword')}
|
||||
/>
|
||||
|
||||
{ errorMsg && <Text color='red' align='center'>{'Error: ' + errorMsg}</Text> }
|
||||
|
||||
<Button type='submit' color='teal' disabled={isLoading} fullWidth>Register</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignUpPage
|
||||
17
services/frontend/src/pages/User.tsx
Normal file
17
services/frontend/src/pages/User.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { Stack } from '@mantine/core'
|
||||
|
||||
import UserPostFeed from '../components/UserPostFeed'
|
||||
import type { UserContext } from '../components/UserLayout'
|
||||
|
||||
function UserPage() {
|
||||
const { user } = useOutletContext<UserContext>()
|
||||
|
||||
return (
|
||||
<Stack my='lg' spacing='md'>
|
||||
<UserPostFeed user={user} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPage
|
||||
17
services/frontend/src/pages/UserAbout.tsx
Normal file
17
services/frontend/src/pages/UserAbout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useOutletContext } from 'react-router-dom'
|
||||
import { Paper, Text } from '@mantine/core'
|
||||
|
||||
import type { UserContext } from '../components/UserLayout'
|
||||
|
||||
function UserAboutPage() {
|
||||
const { user } = useOutletContext<UserContext>()
|
||||
|
||||
return (
|
||||
<Paper mt='md' radius='lg' shadow='md' p='lg' withBorder>
|
||||
<Text weight={500}>About {user.username}</Text>
|
||||
{user.createdAt && <Text>Signed up {new Date(user.createdAt).toUTCString()}</Text>}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserAboutPage
|
||||
43
services/frontend/src/pages/UserSettings.tsx
Normal file
43
services/frontend/src/pages/UserSettings.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom'
|
||||
import { Paper, Text, Button } from '@mantine/core'
|
||||
|
||||
import { setUnauthed } from '../app/features/auth'
|
||||
import { useDeleteUserByIdMutation } from '../app/api/users'
|
||||
import { useAppSelector, useAppDispatch } from '../app/hooks'
|
||||
import type { UserContext } from '../components/UserLayout'
|
||||
|
||||
function UserSettingsPage() {
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const { user } = useOutletContext<UserContext>()
|
||||
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||
if (user && (!currentUser || (currentUser.id != user.id && !currentUser.isAdmin))) {
|
||||
throw Error('You do not have permission to view that page')
|
||||
}
|
||||
|
||||
const [deleteUser, { isLoading }] = useDeleteUserByIdMutation()
|
||||
const submitDeleteAccount = async () => {
|
||||
await deleteUser({id: user.id}).unwrap().then(() => {
|
||||
dispatch(setUnauthed())
|
||||
navigate('/')
|
||||
}).catch((error) => {
|
||||
if (!error.data) {
|
||||
setErrorMsg('Failed to access the API')
|
||||
} else {
|
||||
setErrorMsg(error.data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper mt='md' radius='lg' shadow='md' p='lg' withBorder>
|
||||
<Button color='red' onClick={() => submitDeleteAccount()} disabled={isLoading}>Delete Account</Button>
|
||||
{ errorMsg && <Text color='red'>{'Error: ' + errorMsg}</Text> }
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserSettingsPage
|
||||
Reference in New Issue
Block a user