Refactor components: update CheckActivationCodeModal, Footer, Nav, and PlatformNav; add Reset Password functionality in Login; implement license files.

This commit is contained in:
Fayzan Naufal Suparjo 2025-05-10 16:32:35 +07:00
parent 77922dcfe3
commit 4b926982cf
9 changed files with 494 additions and 159 deletions

21
public/LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Pete Pongpeauk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
public/images/LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Pete Pongpeauk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -116,15 +116,15 @@ export default function CheckActivationCodeModal({
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button onClick={onClose}>Cancel</Button>
<Button <Button
colorScheme={'black'} colorScheme={'black'}
mr={3} ml={3}
isLoading={props.isSubmitting} isLoading={props.isSubmitting}
type={'submit'} type={'submit'}
> >
Submit Submit
</Button> </Button>
<Button onClick={onClose}>Cancel</Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Form> </Form>

View file

@ -47,8 +47,9 @@ export default function Footer({ type = 'platform' }: { type?: 'public' | 'platf
fontWeight={'bold'} fontWeight={'bold'}
letterSpacing={'tight'} letterSpacing={'tight'}
> >
PT Amperra Sambung Semesta Sentosa PT Amperra Sambung Semesta Sentosa{' '}
</Text> </Text>
under the <Link fontWeight={'bold'} as={NextLink} href={'/LICENSE.txt'}>MIT License.</Link>
</Text> </Text>
</Flex> </Flex>
<Flex align={'center'} justify={'center'} fontSize={'sm'}> <Flex align={'center'} justify={'center'} fontSize={'sm'}>

View file

@ -122,6 +122,7 @@ export default function Nav({ type }: { type?: string }) {
h={'6rem'} h={'6rem'}
align={'center'} align={'center'}
bg={useColorModeValue('white', 'gray.800')} bg={useColorModeValue('white', 'gray.800')}
borderBottom={'1px solid'}
borderColor={useColorModeValue('gray.300', 'gray.700')} borderColor={useColorModeValue('gray.300', 'gray.700')}
zIndex={50} zIndex={50}
> >
@ -149,7 +150,7 @@ export default function Nav({ type }: { type?: string }) {
> >
<Flex <Flex
position={'relative'} position={'relative'}
w={'128px'} w={'150px'}
h={'100%'} h={'100%'}
> >
<NextImage <NextImage

View file

@ -368,7 +368,7 @@ export default function PlatformNav({ type, title }: { type?: string; title?: st
> >
<Flex <Flex
position={'relative'} position={'relative'}
w={'128px'} w={'150px'}
h={'100%'} h={'100%'}
> >
<NextImage <NextImage

View file

@ -24,21 +24,25 @@ import {
Text, Text,
useColorModeValue, useColorModeValue,
useDisclosure, useDisclosure,
useToast useToast,
Collapse // Added Collapse
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import NextLink from 'next/link';
// Icons // Icons
import { MdEmail } from 'react-icons/md'; import { MdEmail } from 'react-icons/md';
import { RiLockPasswordFill } from 'react-icons/ri'; import { RiLockPasswordFill } from 'react-icons/ri';
import { BsDisplayFill } from 'react-icons/bs'; // Added
import { FaUser, FaKey } from 'react-icons/fa'; // Added
// Authentication // Authentication
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; import { getAuth, signInWithEmailAndPassword, sendPasswordResetEmail } from 'firebase/auth';
import { Field, Form, Formik } from 'formik'; import { useAuthState } from 'react-firebase-hooks/auth'; // Corrected import for useAuthState
import { Formik, Form, Field } from 'formik';
import Head from 'next/head'; import Head from 'next/head';
import NextLink from 'next/link';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useAuthState } from 'react-firebase-hooks/auth'; import { usePathname } from 'next/navigation';
import { useState } from 'react'; // Added useState
// Components // Components
import CheckActivationCodeModal from '@/components/CheckActivationCodeModal'; import CheckActivationCodeModal from '@/components/CheckActivationCodeModal';
@ -48,17 +52,17 @@ import Layout from '@/layouts/PublicLayout';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [isRegisterMode, setIsRegisterMode] = useState(false); // Added state for register mode
const auth = getAuth(); const auth = getAuth();
const [user, loading, error] = useAuthState(auth); const [user, loading, error] = useAuthState(auth);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isActivationCodeOpen, onOpen: onActivationCodeOpen, onClose: onActivationCodeClose } = useDisclosure(); const { isOpen: isActivationCodeOpen, onOpen: onActivationCodeOpen, onClose: onActivationCodeClose } = useDisclosure();
const { isOpen: isResetModalOpen, onOpen: onResetModalOpen, onClose: onResetModalClose } = useDisclosure(); // New state for reset modal
const toast = useToast(); const toast = useToast();
function redirectOnAuth() { function redirectOnAuth() {
// Check to see if there are any redirect query parameters
// otherwise, redirect to the platform home
if (router.query.redirect) { if (router.query.redirect) {
router.push(router.query.redirect as string); router.push(router.query.redirect as string);
} else { } else {
@ -146,61 +150,230 @@ export default function Login() {
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
<Section>
<Box {/* Reset Password Modal */}
position={'relative'} <Modal
minH={"calc(100dvh - 6rem)"} onClose={onResetModalClose}
h={'calc(100dvh - 6rem)'} isOpen={isResetModalOpen}
isCentered
size={'md'}
> >
<ModalOverlay />
<ModalContent bg={useColorModeValue('white', 'gray.800')}>
<ModalHeader>Reset Password</ModalHeader>
<ModalCloseButton />
<Formik
initialValues={{ email: '' }}
onSubmit={(values, actions) => {
sendPasswordResetEmail(auth, values.email)
.then(() => {
toast({
title: 'Password reset email sent.',
description: 'Please check your inbox to reset your password.',
status: 'success',
duration: 9000,
isClosable: true,
});
onResetModalClose();
})
.catch((error) => {
const errorCode = error.code;
let errorMessage = error.message;
switch (errorCode) {
case 'auth/invalid-email':
errorMessage = "The email address you've entered is invalid. Please try again.";
break;
case 'auth/user-not-found':
errorMessage = "No account found with that email address.";
break;
default:
errorMessage = 'An unknown error occurred. Please try again.';
}
toast({
title: 'Error sending reset email',
description: errorMessage,
status: 'error',
duration: 5000,
isClosable: true,
});
})
.finally(() => {
actions.setSubmitting(false);
});
}}
>
{(props) => (
<Form>
<ModalBody>
<Text mb={4}>Enter your email address below and we'll send you a link to reset your password.</Text>
<Field name="email">
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.email && form.touched.email}>
<FormLabel htmlFor="email-reset">Email address</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<MdEmail color="gray.300" />
</InputLeftElement>
<Input {...field} id="email-reset" placeholder="you@example.com" />
</InputGroup>
</FormControl>
)}
</Field>
</ModalBody>
<ModalFooter>
<Button variant="ghost" mr={3} onClick={onResetModalClose}>
Cancel
</Button>
<Button
colorScheme="blue"
isLoading={props.isSubmitting}
type="submit"
>
Send Reset Email
</Button>
</ModalFooter>
</Form>
)}
</Formik>
</ModalContent>
</Modal>
<Section>
<Flex <Flex
position={'relative'} position={'relative'}
align={'center'} height={"calc(100dvh - 6rem)"} // Changed from minH to height
justify={'center'} w="full"
height={'100%'}
bottom={{ base: '0', md: '3em' }}
> >
{ {/* Background Image */}
!loading && !user ? <Image
(<Flex position="absolute"
position={'relative'} top={0}
flexDir={'row'} left={0}
align={{ base: 'center', md: 'flex-start' }} right={0}
outline={['0px solid', '1px solid']} bottom={0}
outlineColor={['unset', useColorModeValue('gray.200', 'gray.700')]} src={'/images/login4.png'}
rounded={'lg'} alt={'Background'}
maxW={"container.md"} objectFit={'cover'}
overflow={'hidden'}
height={{ base: 'auto', md: '500px' }}
boxShadow={'rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;'}
>
<Image flex={"0 0 auto"} display={{ base: "none", md: "flex" }} src={'/images/login4.png'} alt={'Login'} objectFit={'cover'} w={'sm'} h={"full"} />
{/* <Flex flex={"0 0 auto"} display={{ base: "none", md: "flex" }} objectFit={'cover'} w={'sm'} h={"full"} flexDir={'column'}>
<Box
backgroundColor={useColorModeValue('gray.200', 'gray.700')}
w={'full'} w={'full'}
h={'full'} h={'full'}
zIndex={0}
/> />
</Flex> */}
<Flex flex={"1 1 auto"} flexDir={'column'} justify={"center"} align={"flex-start"} py={8} px={10}> {/* Login Panel (Left Side) */}
<Box <Flex
w={'full'} position="relative"
zIndex={1}
w={user ? 'full' : { base: 'full', md: '480px' }} // Conditionally set width
h="full" // This will now refer to the explicit height of the parent Flex
bg={useColorModeValue('white', 'gray.800')}
transition="width 0.6s ease-in-out" // Added transition property
// backdropFilter="blur(8px)" // Optional: for a frosted glass effect
direction="column"
alignItems="center"
justifyContent="center"
p={{ base: 6, md: 10 }}
> >
{
loading ? (
<Spinner size="xl" />
) : !user ? (
<Flex
direction="column"
w="full"
maxW="md" // Max width for the form elements
align="stretch"
>
<Box w={'full'} textAlign={{ base: "center", md: "left" }}>
<Text <Text
fontSize={"3xl"} fontSize={"3xl"}
fontWeight={'bold'} fontWeight={'bold'}
> >
Welcome! Welcome!
</Text> </Text>
<Text color={"gray.500"} fontSize={'md'}>Please present your credentials to continue.</Text> <Text color={useColorModeValue("gray.600", "gray.400")} fontSize={'md'}>Please present your credentials to continue.</Text>
</Box> </Box>
<br /> <br />
<Box px={[0, 4]} w={"full"}> <Box w={"full"}>
<Formik <Formik
initialValues={{ initialValues={{
email: '', email: '',
password: '', password: '',
displayName: '', // Added
username: '', // Added
activationCode: '', // Added
confirmPassword: '' // Added for confirm password
}} }}
onSubmit={(values, actions) => { onSubmit={(values, actions) => {
if (isRegisterMode) {
// Registration Logic
if (values.password !== values.confirmPassword) {
toast({
title: 'Passwords do not match.',
description: 'Please ensure your password and confirmation match.',
status: 'error',
duration: 5000,
isClosable: true
});
actions.setSubmitting(false);
return;
}
if (!values.activationCode) {
toast({
title: 'Activation code required.',
description: 'Please enter your activation code.',
status: 'error',
duration: 5000,
isClosable: true
});
actions.setSubmitting(false);
return;
}
fetch(`/api/v1/activation/${values.activationCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
displayName: values.displayName,
email: values.email,
username: values.username,
password: values.password
})
})
.then((res) => {
if (res.status === 200) {
return res.json();
} else {
return res.json().then((json) => {
throw new Error(json.message || 'Registration failed');
});
}
})
.then(() => {
toast({
title: 'Account created.',
description: 'You can now log in.',
status: 'success',
duration: 5000,
isClosable: true
});
setIsRegisterMode(false); // Switch back to login mode
actions.resetForm();
})
.catch((error) => {
toast({
title: 'There was an error while creating your account.',
description: error.message,
status: 'error',
duration: 5000,
isClosable: true
});
})
.finally(() => {
actions.setSubmitting(false);
});
} else {
// Login Logic
signInWithEmailAndPassword(auth, values.email, values.password) signInWithEmailAndPassword(auth, values.email, values.password)
.then(() => { .then(() => {
redirectOnAuth(); redirectOnAuth();
@ -212,20 +385,17 @@ export default function Login() {
case 'auth/invalid-email': case 'auth/invalid-email':
errorMessage = 'The email address you provided is invalid.'; errorMessage = 'The email address you provided is invalid.';
break; break;
case 'auth/invalid-password': case 'auth/invalid-credential':
case 'auth/user-not-found':
case 'auth/wrong-password':
errorMessage = "Invalid email address or password. Please try again."; errorMessage = "Invalid email address or password. Please try again.";
break; break;
case 'auth/user-disabled': case 'auth/user-disabled':
errorMessage = 'Your account has been disabled.'; errorMessage = 'Your account has been disabled.';
break; break;
case 'auth/user-not-found':
errorMessage = "Invalid email address or password. Please try again.";
break;
case 'auth/wrong-password':
errorMessage = "Invalid email address or password. Please try again.";
break;
case 'auth/too-many-requests': case 'auth/too-many-requests':
errorMessage = 'Too many attempts. Please try again later.'; errorMessage = 'Too many attempts. Please try again later.';
break;
default: default:
errorMessage = 'An unknown error occurred.'; errorMessage = 'An unknown error occurred.';
} }
@ -239,13 +409,14 @@ export default function Login() {
.finally(() => { .finally(() => {
actions.setSubmitting(false); actions.setSubmitting(false);
}); });
}
}} }}
> >
{(props) => ( {(props) => (
<Form> <Form>
<Field name="email"> <Field name="email">
{({ field, form }: any) => ( {({ field, form }: any) => (
<FormControl mt={2}> <FormControl mt={2} isRequired isInvalid={form.errors.email && form.touched.email}>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<InputGroup> <InputGroup>
<InputLeftElement pointerEvents="none"> <InputLeftElement pointerEvents="none">
@ -253,7 +424,7 @@ export default function Login() {
</InputLeftElement> </InputLeftElement>
<Input <Input
{...field} {...field}
type="text" type="email"
placeholder="Email address" placeholder="Email address"
variant={'outline'} variant={'outline'}
/> />
@ -263,7 +434,7 @@ export default function Login() {
</Field> </Field>
<Field name="password"> <Field name="password">
{({ field, form }: any) => ( {({ field, form }: any) => (
<FormControl my={2}> <FormControl mt={2} isRequired isInvalid={form.errors.password && form.touched.password}>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<InputGroup> <InputGroup>
<InputLeftElement pointerEvents="none"> <InputLeftElement pointerEvents="none">
@ -271,7 +442,7 @@ export default function Login() {
</InputLeftElement> </InputLeftElement>
<Input <Input
{...field} {...field}
type={'password'} type="password"
placeholder="Password" placeholder="Password"
variant={'outline'} variant={'outline'}
/> />
@ -279,35 +450,155 @@ export default function Login() {
</FormControl> </FormControl>
)} )}
</Field> </Field>
<Collapse in={isRegisterMode} animateOpacity>
<Box>
<Field name="confirmPassword">
{({ field, form }: any) => (
<FormControl mt={2} isRequired={isRegisterMode} isInvalid={form.errors.confirmPassword && form.touched.confirmPassword}>
<FormLabel>Confirm Password</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<RiLockPasswordFill color="gray.300" />
</InputLeftElement>
<Input
{...field}
type="password"
placeholder="Confirm Password"
variant={'outline'}
/>
</InputGroup>
</FormControl>
)}
</Field>
<Flex direction={{ base: 'column', md: 'row' }} mt={2} gap={2}>
<Field name="displayName">
{({ field, form }: any) => (
<FormControl mt={{ base: 2, md: 0 }} isRequired={isRegisterMode} isInvalid={form.errors.displayName && form.touched.displayName} flex={1}>
<FormLabel>Display Name</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<BsDisplayFill color="gray.300" />
</InputLeftElement>
<Input
{...field}
type="text"
placeholder="Display Name"
variant={'outline'}
/>
</InputGroup>
</FormControl>
)}
</Field>
<Field name="username">
{({ field, form }: any) => (
<FormControl mt={{ base: 2, md: 0 }} isRequired={isRegisterMode} isInvalid={form.errors.username && form.touched.username} flex={1}>
<FormLabel>Username</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<FaUser color="gray.300" />
</InputLeftElement>
<Input
{...field}
type="text"
placeholder="Username"
variant={'outline'}
/>
</InputGroup>
</FormControl>
)}
</Field>
</Flex>
<Field name="activationCode">
{({ field, form }: any) => (
<FormControl mt={2} isRequired={isRegisterMode} isInvalid={form.errors.activationCode && form.touched.activationCode}>
<FormLabel>Activation Code</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents="none">
<FaKey color="gray.300" />
</InputLeftElement>
<Input
{...field}
type="text"
placeholder="Activation Code"
variant={'outline'}
/>
</InputGroup>
</FormControl>
)}
</Field>
</Box>
</Collapse>
<Button <Button
my={2} my={4}
w={'full'} w={'full'}
isLoading={props.isSubmitting} isLoading={props.isSubmitting}
type={'submit'} type={'submit'}
> >
Log in {isRegisterMode ? 'Create Account' : 'Log in'}
</Button> </Button>
</Form> </Form>
)} )}
</Formik> </Formik>
{isRegisterMode ? (
<Text fontSize={'sm'} mt={2} textAlign={{ base: "center", md: "left" }}>
Already have an account?{' '}
<Link onClick={() => setIsRegisterMode(false)} cursor="pointer" textDecor={'underline'} textUnderlineOffset={4}>
Login
</Link>
</Text>
) : (
<Button
w={'full'}
variant={'outline'}
onClick={() => setIsRegisterMode(true)}
>
Create an Account
</Button>
)}
{isRegisterMode && (
<Text fontSize={'sm'}> <Text fontSize={'sm'}>
By creating an account, you agree to our{' '}
<Text as={'span'}>
<Link <Link
as={NextLink} as={NextLink}
href="/auth/reset" href={'/legal/terms'}
textDecor={'underline'}
textUnderlineOffset={4} textUnderlineOffset={4}
whiteSpace={'nowrap'}
>
Terms of Use
</Link>
</Text>{' '}
and{' '}
<Text as={'span'}>
<Link
as={NextLink}
href={'/legal/privacy'}
textDecor={'underline'}
textUnderlineOffset={4}
whiteSpace={'nowrap'}
>
Privacy Policy
</Link>
</Text>
.
</Text>
)}
<Text fontSize={'sm'} mt={4} textAlign={{ base: "center", md: "left" }}>
<Link
onClick={onResetModalOpen}
textUnderlineOffset={4}
cursor="pointer"
_hover={{ textDecoration: 'underline' }}
> >
Forgot your password? Forgot your password?
</Link> </Link>
</Text> </Text>
<Text fontSize={"sm"}> <Text fontSize={'sm'} mt={2} textAlign={{ base: "center", md: "left" }}>
<Link
onClick={onActivationCodeOpen}
textUnderlineOffset={4}
>
Have an activation code?
</Link>
</Text>
<Text fontSize={'sm'}>
Need help?{' '} Need help?{' '}
<Box <Box
as="button" as="button"
@ -315,22 +606,22 @@ export default function Login() {
textDecor={'underline'} textDecor={'underline'}
textUnderlineOffset={4} textUnderlineOffset={4}
transition={'all 0.15s ease'} transition={'all 0.15s ease'}
_hover={{ color: ['gray.300', 'gray.500'] }} _hover={{ color: useColorModeValue('gray.600', 'gray.400') }}
> >
View the FAQ. View the FAQ.
</Box> </Box>
</Text> </Text>
</Box> </Box>
</Flex> </Flex>
</Flex> ) : (
) : <> // User is logged in, redirectOnAuth has been called. Show spinner during transition.
<AbsoluteCenter> <Spinner size="xl" />
<Spinner /> )
</AbsoluteCenter>
</>
} }
</Flex> </Flex>
</Box> </Flex>
</Section> </Section>
</> </>
); );