Skip to content

Commit f2fbba4

Browse files
authored
Upgrade NextJS 13 -> 15 (#76)
* Run next/codemod Excludes Yarn files * Lockfile * Fixes * Change login to a server action Hopefully it's more reliable ?? but idk how to test. This fixes an issue with the navbar not updating whne logging in though * Convert remaining routes to server actions This also fixes an issue of the name in the navbar not updating (because of the stale USER cookie) * fix other checkbox not being selectable by its label
1 parent fad6d89 commit f2fbba4

File tree

14 files changed

+543
-454
lines changed

14 files changed

+543
-454
lines changed

client/next.config.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ const nextConfig = {
3232
},
3333
];
3434
},
35-
experimental: {
36-
serverActions: true,
37-
},
3835
};
3936

4037
module.exports = nextConfig;

client/package.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,29 @@
1616
"@mui/material": "^6.2.1",
1717
"@svgr/webpack": "^8.1.0",
1818
"@types/node": "20.6.0",
19-
"@types/react": "18.2.21",
20-
"@types/react-dom": "18.2.7",
19+
"@types/react": "19.0.8",
20+
"@types/react-dom": "19.0.3",
2121
"@types/validator": "^13.12.2",
2222
"axios": "^1.7.9",
2323
"cookies-next": "^5.0.2",
2424
"localforage": "^1.10.0",
25-
"next": "13.4.16",
26-
"react": "18.2.0",
27-
"react-dom": "18.2.0",
25+
"next": "15.1.6",
26+
"react": "19.0.0",
27+
"react-dom": "19.0.0",
2828
"react-hook-form": "^7.54.2",
2929
"react-toastify": "^11.0.2",
3030
"typescript": "5.2.2",
3131
"validator": "^13.12.0"
3232
},
3333
"devDependencies": {
3434
"eslint": "^8.49.0",
35-
"eslint-config-next": "^13.4.19",
35+
"eslint-config-next": "15.1.6",
3636
"eslint-config-prettier": "^9.0.0",
3737
"prettier": "^3.0.3",
3838
"sass": "^1.70.0"
39+
},
40+
"resolutions": {
41+
"@types/react": "19.0.8",
42+
"@types/react-dom": "19.0.3"
3943
}
4044
}

client/src/app/api/getCurrentUser/route.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

client/src/app/api/login/route.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

client/src/app/api/updateUser/route.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

client/src/app/check-email/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import Button from '@/components/Button';
55
import styles from './page.module.scss';
66

77
interface CheckEmailProps {
8-
searchParams: { email?: string };
8+
searchParams: Promise<{ email?: string }>;
99
}
1010

11-
const CheckEmail = ({ searchParams }: CheckEmailProps) => {
11+
const CheckEmail = async (props: CheckEmailProps) => {
12+
const searchParams = await props.searchParams;
1213
const email = searchParams.email;
1314

1415
return (

client/src/app/login/login.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use server';
2+
3+
import { login as apiLogin } from '@/lib/api/UserAPI';
4+
import { setCookie } from '@/lib/services/CookieService';
5+
import { CookieType } from '@/lib/types/enums';
6+
import { getErrorMessage } from '@/lib/utils';
7+
import { redirect } from 'next/navigation';
8+
9+
export async function login(email: string, password: string): Promise<string> {
10+
let response;
11+
try {
12+
response = await apiLogin(email, password);
13+
} catch (error) {
14+
return getErrorMessage(error);
15+
}
16+
await setCookie(CookieType.ACCESS_TOKEN, response.token);
17+
await setCookie(CookieType.USER, JSON.stringify(response.user));
18+
redirect('/');
19+
}

client/src/app/login/page.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { UserAPI } from '@/lib/api';
1313
import { useRouter } from 'next/navigation';
1414
import { useState } from 'react';
1515
import { getErrorMessage } from '@/lib/utils';
16+
import { login } from './login';
1617

1718
interface LoginValues {
1819
email: string;
@@ -30,13 +31,10 @@ export default function LoginPage() {
3031
} = useForm<LoginValues>();
3132

3233
const onSubmit: SubmitHandler<LoginValues> = async credentials => {
33-
try {
34-
await UserAPI.login(credentials.email, credentials.password);
35-
router.refresh();
36-
router.push('/');
37-
} catch (error) {
38-
setError(getErrorMessage(error));
39-
}
34+
// If successful, the page will redirect and the rest of this function will
35+
// not run
36+
const error = await login(credentials.email, credentials.password);
37+
setError(error);
4038
};
4139

4240
return (

client/src/components/Dashboard/style.module.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@use '@/styles/vars.scss' as vars;
2+
@use 'sass:list';
23

34
.container {
45
display: grid;
@@ -67,7 +68,7 @@
6768
// Wrap each shadow in drop-shadow()
6869
$drop-shadows: ();
6970
@each $shadow in vars.$text-shadow {
70-
$drop-shadows: append($drop-shadows, drop-shadow(#{$shadow}));
71+
$drop-shadows: list.append($drop-shadows, drop-shadow(#{$shadow}));
7172
}
7273
filter: #{$drop-shadows};
7374

client/src/components/MultipleChoiceGroup/index.tsx

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

3-
import { Checkbox, Radio } from '@mui/material';
3+
import { Checkbox, CheckboxProps, Radio, RadioProps } from '@mui/material';
44
import styles from './style.module.scss';
55
import { useRef, useState } from 'react';
66

@@ -55,31 +55,33 @@ const MultipleChoiceGroup = ({
5555
return (
5656
<div className={`${styles.wrapper} ${inline ? styles.inline : ''}`} ref={ref}>
5757
{choices.map(choice => {
58+
const props: RadioProps & CheckboxProps = {
59+
name,
60+
value: choice,
61+
// For checkboxes, we can require at least one checkbox by
62+
// only setting all of them `required` if none of them are
63+
// checked
64+
required: required && (mode === 'radio' || selected === NONE),
65+
onChange: e => {
66+
if (e.currentTarget.checked) {
67+
setSelected(choice);
68+
} else if (mode === 'checkbox' && !ref.current?.querySelector(':checked')) {
69+
setSelected(NONE);
70+
}
71+
},
72+
disabled,
73+
};
74+
// React has a distinction between undefined and whether the prop is
75+
// present at all, and these two props can't both be present
76+
if (mode === 'radio') {
77+
props.checked = selected === choice;
78+
} else if (defaultValue !== undefined) {
79+
props.defaultChecked = Array.isArray(defaultValue) && defaultValue.includes(choice);
80+
}
5881
return (
5982
<p key={choice}>
6083
<label className={styles.checkboxLabel}>
61-
<Component
62-
name={name}
63-
value={choice}
64-
// For checkboxes, we can require at least one checkbox by
65-
// only setting all of them `required` if none of them are
66-
// checked
67-
required={required && (mode === 'radio' || selected === NONE)}
68-
checked={mode === 'radio' ? selected === choice : undefined}
69-
defaultChecked={
70-
defaultValue === undefined || mode === 'radio'
71-
? undefined
72-
: Array.isArray(defaultValue) && defaultValue.includes(choice)
73-
}
74-
onChange={e => {
75-
if (e.currentTarget.checked) {
76-
setSelected(choice);
77-
} else if (mode === 'checkbox' && !ref.current?.querySelector(':checked')) {
78-
setSelected(NONE);
79-
}
80-
}}
81-
disabled={disabled}
82-
/>
84+
<Component {...props} />
8385
{choice}
8486
</label>
8587
</p>
@@ -88,7 +90,10 @@ const MultipleChoiceGroup = ({
8890
{other ? (
8991
<p
9092
className={styles.checkboxLabel}
91-
onClick={() => {
93+
onClick={e => {
94+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLLabelElement) {
95+
return;
96+
}
9297
setShowOther(true);
9398
setSelected(OTHER);
9499
}}
@@ -99,9 +104,6 @@ const MultipleChoiceGroup = ({
99104
value={OTHER}
100105
required={required && (mode === 'radio' || selected === NONE)}
101106
checked={isOtherEnabled}
102-
defaultChecked={
103-
defaultValue === undefined || mode === 'radio' ? undefined : defaultOther !== null
104-
}
105107
onChange={e => {
106108
setShowOther(e.currentTarget.checked);
107109
if (e.currentTarget.checked) {

client/src/components/Profile/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import TextField from '@/components/TextField';
99
import CloseIcon from '../../../public/assets/icons/close.svg';
1010
import EditIcon from '../../../public/assets/icons/edit.svg';
1111
import { UserAPI } from '@/lib/api';
12-
import { reportError } from '@/lib/utils';
1312
import { useWindowSize } from '@/lib/hooks/useWindowSize';
1413
import isEmail from 'validator/lib/isEmail';
1514
import styles from './style.module.scss';
1615
import logout from './logout';
16+
import showToast from '@/lib/showToast';
1717

1818
interface UpdateProfileValues {
1919
firstName: string;
@@ -45,13 +45,13 @@ const Profile = ({ user }: ProfileClientProps) => {
4545
});
4646

4747
const onSubmit: SubmitHandler<UpdateProfileValues> = async updateProfile => {
48-
try {
49-
const updatedUser = await UserAPI.updateCurrentUserProfile(updateProfile);
48+
const updatedUser = await UserAPI.updateCurrentUserProfile(updateProfile);
49+
if (typeof updatedUser === 'string') {
50+
showToast('Changes failed to save', updatedUser);
51+
} else {
5052
setCurrentUser(updatedUser);
5153
setEditProfile(prevState => !prevState);
5254
reset(updatedUser);
53-
} catch (error) {
54-
reportError('Changes failed to save', error);
5555
}
5656
};
5757

client/src/components/Profile/logout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { cookies } from 'next/headers';
55
import { redirect } from 'next/navigation';
66

77
export default async function logout() {
8-
const cookieStore = cookies();
8+
const cookieStore = await cookies();
99
cookieStore.delete(CookieType.ACCESS_TOKEN);
1010
cookieStore.delete(CookieType.USER);
1111
redirect('/login');

0 commit comments

Comments
 (0)