Skip to content

Commit 16fcecb

Browse files
authored
Merge pull request #3 from AlexStack/nextjs14
feat: add AlertBar & styled component example
2 parents 79a488c + 83c510b commit 16fcecb

File tree

4 files changed

+127
-15
lines changed

4 files changed

+127
-15
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# NextJs 14.x + MUI 5.x + React Hook Form + TypeScript Starter and Boilerplate
22

33
<div align="center">
4-
<h2>2023/2024: 🔋 NextJs 14.x + MUI 5.x + TypeScript Starter</h2>
4+
<h2>2024/2025: 🔋 NextJs 14.x + MUI 5.x + TypeScript Starter</h2>
55
<p>The scaffold for NextJs 14.x (App Router), React Hook Form, Material UI(MUI 5.x),Typescript and ESLint, and TypeScript with Absolute Import, Seo, Link component, pre-configured with Husky.</p>
66

7-
<p>With simple example of NextJs API, React-hook-form with zod, fetch remote api, 404/500 error pages, MUI SSR usage</p>
7+
<p>With simple example of NextJs API, React-hook-form with zod, fetch remote api, 404/500 error pages, MUI SSR usage, Styled component, MUI AlertBar, Client-side component</p>
88

99
🚘🚘🚘 [**Click here to see an online demo**](https://mui-nextjs-ts.vercel.app) 🚘🚘🚘
1010

src/components/Homepage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default function Homepage({
7676

7777
<Box sx={{ m: 5 }}>
7878
<h4>
79-
Test local NextJs API /api/test method POST with form variables
79+
Test local NextJs API /api/tes POST method (client-side component)
8080
</h4>
8181
<ReactHookForm />
8282
</Box>

src/components/shared/AlertBar.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import CloseIcon from '@mui/icons-material/Close';
2+
import {
3+
Alert,
4+
AlertColor,
5+
IconButton,
6+
Slide,
7+
Snackbar,
8+
SnackbarOrigin,
9+
} from '@mui/material';
10+
11+
export interface AlertBarProps {
12+
message: React.ReactNode;
13+
onClose?: () => void;
14+
severity?: AlertColor;
15+
vertical?: SnackbarOrigin['vertical'];
16+
horizontal?: SnackbarOrigin['horizontal'];
17+
autoHideSeconds?: number;
18+
transitionSeconds?: number;
19+
}
20+
21+
// my personal alert bar, feel free to use it, examples are in ReactHookForm.tsx
22+
export const AlertBar = ({
23+
message,
24+
severity = 'info',
25+
vertical = 'bottom',
26+
horizontal = 'center',
27+
onClose,
28+
autoHideSeconds = 5,
29+
transitionSeconds = 0.5,
30+
}: AlertBarProps) => {
31+
return message ? (
32+
<Snackbar
33+
anchorOrigin={{ vertical, horizontal }}
34+
open={!!message}
35+
onClose={onClose}
36+
autoHideDuration={autoHideSeconds * 1000}
37+
TransitionComponent={(props) => <Slide {...props} direction='up' />}
38+
transitionDuration={transitionSeconds * 1000}
39+
action={
40+
<IconButton
41+
size='small'
42+
aria-label='close'
43+
color='inherit'
44+
onClick={onClose}
45+
>
46+
<CloseIcon fontSize='small' />
47+
</IconButton>
48+
}
49+
>
50+
<Alert
51+
onClose={onClose}
52+
severity={severity}
53+
variant='filled'
54+
sx={{ width: '100%' }}
55+
>
56+
{message}
57+
</Alert>
58+
</Snackbar>
59+
) : null;
60+
};

src/components/shared/ReactHookForm.tsx

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,52 @@
11
'use client';
22

3+
import styled from '@emotion/styled';
34
import { zodResolver } from '@hookform/resolvers/zod';
4-
import { Box, Button, FormHelperText } from '@mui/material';
5-
import React from 'react';
5+
import { Save } from '@mui/icons-material';
6+
import { Box, Button, FormHelperText, TextField } from '@mui/material';
7+
import React, { useEffect, useState } from 'react';
68
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
79
import { z } from 'zod';
810

11+
import { AlertBar, AlertBarProps } from '@/components/shared/AlertBar';
12+
913
import { consoleLog } from '@/utils/shared/console-log';
1014
import { getApiResponse } from '@/utils/shared/get-api-response';
1115

1216
const zodSchema = z.object({
13-
name: z.string().min(5).nonempty({ message: 'Name is required' }),
14-
email: z.string().min(10).email({ message: 'Invalid email address' }),
17+
name: z
18+
.string()
19+
.min(3, { message: 'Name must contain at least 3 characters' })
20+
.nonempty({ message: 'Name is required' }),
21+
email: z
22+
.string()
23+
.min(10, { message: 'Email must contain at least 10 characters' })
24+
.email({ message: 'Invalid email address' }),
1525
});
1626

27+
const StyledForm = styled.form`
28+
.MuiFormHelperText-root {
29+
text-align: center;
30+
color: darkred;
31+
margin-bottom: 1rem;
32+
}
33+
`;
34+
1735
type FormValues = z.infer<typeof zodSchema>;
1836

1937
const ReactHookForm: React.FC = () => {
2038
const apiEndpoint = '/api/test';
2139
const [apiResult, setApiResult] = React.useState<FormValues>();
2240

41+
const [alertBarProps, setAlertBarProps] = useState<AlertBarProps>({
42+
message: '',
43+
severity: 'info',
44+
});
45+
2346
const {
2447
handleSubmit,
2548
control,
26-
formState: { errors },
49+
formState: { errors, isValid },
2750
} = useForm<FormValues>({
2851
resolver: zodResolver(zodSchema),
2952
});
@@ -39,20 +62,42 @@ const ReactHookForm: React.FC = () => {
3962
});
4063
setApiResult(result?.reqData);
4164
consoleLog('getApiResponse result', result, errors);
65+
66+
setAlertBarProps({
67+
message: 'Form submitted successfully',
68+
severity: 'success',
69+
});
4270
} catch (error) {
4371
consoleLog('handleSubmit ERROR', error);
72+
73+
setAlertBarProps({
74+
message: 'Form submission failed',
75+
severity: 'error',
76+
});
4477
}
4578
};
4679

80+
useEffect(() => {
81+
if (!isValid && Object.keys(errors).length > 0) {
82+
setAlertBarProps({
83+
message: 'Please fix the form errors',
84+
severity: 'warning',
85+
autoHideSeconds: 4,
86+
});
87+
}
88+
}, [isValid, errors]);
89+
4790
return (
48-
<form onSubmit={handleSubmit(onSubmit)}>
91+
<StyledForm onSubmit={handleSubmit(onSubmit)}>
4992
<Box sx={{ m: 2 }}>
50-
<label>Name:</label>
93+
{/* <label>Name:</label> */}
5194
<Controller
5295
name='name'
5396
control={control}
5497
defaultValue=''
55-
render={({ field }) => <input {...field} />}
98+
render={({ field }) => (
99+
<TextField label='Name' {...field} size='small' />
100+
)}
56101
/>
57102
{errors.name && (
58103
<FormHelperText sx={{ textAlign: 'center' }}>
@@ -62,12 +107,14 @@ const ReactHookForm: React.FC = () => {
62107
</Box>
63108

64109
<Box sx={{ m: 2 }}>
65-
<label>Email:</label>
110+
{/* <label>Email:</label> */}
66111
<Controller
67112
name='email'
68113
control={control}
69114
defaultValue=''
70-
render={({ field }) => <input {...field} />}
115+
render={({ field }) => (
116+
<TextField label='Email' {...field} size='small' />
117+
)}
71118
/>
72119
{errors.email && (
73120
<FormHelperText sx={{ textAlign: 'center' }}>
@@ -80,10 +127,15 @@ const ReactHookForm: React.FC = () => {
80127
API result from {apiEndpoint}: {apiResult.name} & {apiResult.email}
81128
</Box>
82129
)}
83-
<Button variant='contained' type='submit'>
130+
<Button variant='contained' type='submit' startIcon={<Save />}>
84131
Test react hook form with zod
85132
</Button>
86-
</form>
133+
134+
<AlertBar
135+
onClose={() => setAlertBarProps({ message: '' })}
136+
{...alertBarProps}
137+
/>
138+
</StyledForm>
87139
);
88140
};
89141

0 commit comments

Comments
 (0)