Skip to content

Commit 3c87c31

Browse files
authored
add: 404, ErrorBoundary, Loading component (freeCodeCamp-2025-Summer-Hackathon#110)
1 parent 10cf01e commit 3c87c31

File tree

7 files changed

+709
-20
lines changed

7 files changed

+709
-20
lines changed

frontend/src/App.jsx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { Routes, Route } from 'react-router-dom';
33
import { Helmet } from '@dr.pogodin/react-helmet';
44
import Header from './components/Header';
@@ -12,7 +12,18 @@ import RegisterPage from './pages/RegisterPage';
1212
import { IdeaAddPage, IdeaEditPage, IdeaPage, IdeasPage } from './pages/Ideas';
1313
import './styles.css';
1414

15+
import ErrorBoundary from './components/ErrorBoundary.jsx';
16+
import NotFound from './pages/NotFound.jsx';
17+
18+
import { devLog } from './utils/devLogger.jsx';
19+
import { DevOnly } from './components/DevOnly';
20+
1521
function App() {
22+
useEffect(() => {
23+
devLog('App component has mounted.');
24+
devLog('Current environment mode:', import.meta.env.MODE);
25+
}, []);
26+
1627
return (
1728
<div id='top' className='body-style'>
1829
<Helmet>
@@ -23,25 +34,32 @@ function App() {
2334
/>
2435
</Helmet>
2536
<div className='container-wrapper'>
26-
<Header />
27-
<Routes>
28-
<Route path='' element={<LandingPage />} />
29-
<Route path='help' element={<HelpPage />} />
30-
<Route path='login' element={<LoginPage />} />
31-
<Route path='forgot-password' element={<ForgotPassword />} />
32-
<Route path='register' element={<RegisterPage />} />
33-
<Route path='ideas'>
34-
<Route index element={<IdeasPage headerText='All Ideas' />} />
35-
<Route path=':ideaId' element={<IdeaPage />} />
36-
<Route path=':ideaId/edit' element={<IdeaEditPage />} />
37-
<Route path='add' element={<IdeaAddPage />} />
38-
<Route
39-
path='page/:page'
40-
element={<IdeasPage headerText='All Ideas' />}
41-
/>
42-
</Route>
43-
</Routes>
44-
<Footer />
37+
<ErrorBoundary>
38+
<Header />
39+
<Routes>
40+
<Route path='' element={<LandingPage />} />
41+
<Route path='help' element={<HelpPage />} />
42+
<Route path='login' element={<LoginPage />} />
43+
<Route path='forgot-password' element={<ForgotPassword />} />
44+
<Route path='register' element={<RegisterPage />} />
45+
<Route path='ideas'>
46+
<Route index element={<IdeasPage headerText='All Ideas' />} />
47+
<Route path=':ideaId' element={<IdeaPage />} />
48+
<Route path=':ideaId/edit' element={<IdeaEditPage />} />
49+
<Route path='add' element={<IdeaAddPage />} />
50+
<Route
51+
path='page/:page'
52+
element={<IdeasPage headerText='All Ideas' />}
53+
/>
54+
</Route>
55+
<Route path='*' element={<NotFound />} />
56+
</Routes>
57+
<Footer />
58+
59+
<DevOnly>
60+
<div className='dev-indicator'>DEV</div>
61+
</DevOnly>
62+
</ErrorBoundary>
4563
</div>
4664
</div>
4765
);

frontend/src/assets/404.svg

Lines changed: 526 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function DevOnly({ children }) {
2+
if (import.meta.env.DEV) {
3+
return <>{children}</>;
4+
}
5+
return null;
6+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
3+
class ErrorBoundary extends React.Component {
4+
constructor(props) {
5+
super(props);
6+
this.state = { hasError: false, error: null, errorInfo: null };
7+
}
8+
9+
static getDerivedStateFromError(_error) {
10+
void _error;
11+
return { hasError: true };
12+
}
13+
14+
componentDidCatch(error, errorInfo) {
15+
console.error('ErrorBoundary caught an error:', error, errorInfo);
16+
this.setState({
17+
error: error,
18+
errorInfo: errorInfo,
19+
});
20+
21+
if (import.meta.env.DEV) {
22+
console.log('Development mode error details:');
23+
console.log('Error:', error);
24+
console.log('Error Info:', errorInfo);
25+
}
26+
}
27+
28+
render() {
29+
if (this.state.hasError) {
30+
return (
31+
<div className='error-boundary-fallback'>
32+
<h1>Something went wrong.</h1>
33+
<p>
34+
We're sorry for the inconvenience. Please try refreshing the page.
35+
</p>
36+
{import.meta.env.DEV && this.state.error && (
37+
<details style={{ whiteSpace: 'pre-wrap' }}>
38+
{this.state.error.toString()}
39+
<br />
40+
{this.state.errorInfo.componentStack}
41+
</details>
42+
)}
43+
</div>
44+
);
45+
}
46+
47+
return this.props.children;
48+
}
49+
}
50+
51+
export default ErrorBoundary;

frontend/src/pages/NotFound.jsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Link } from 'react-router-dom';
2+
import NotFoundImage from '../assets/404.svg';
3+
4+
const NotFound = () => {
5+
return (
6+
<div
7+
style={{
8+
textAlign: 'center',
9+
padding: 'var(--spacing-lg)',
10+
marginTop: 'var(--spacing-lg)',
11+
backgroundColor: 'var(--palette-accent-white)',
12+
borderRadius: 'var(--border-radius-md)',
13+
boxShadow: 'var(--shadow-sm)',
14+
maxWidth: '600px',
15+
margin: 'var(--spacing-lg) auto',
16+
}}
17+
>
18+
<img
19+
src={NotFoundImage}
20+
alt='404 Not Found'
21+
style={{
22+
maxWidth: '90%',
23+
height: 'auto',
24+
display: 'block',
25+
margin: 'var(--spacing-lg) auto var(--spacing-md) auto',
26+
}}
27+
/>
28+
29+
<Link
30+
to='/'
31+
className='not-found-button'
32+
style={{ marginTop: 'var(--spacing-md)' }}
33+
>
34+
Go to Home Page
35+
</Link>
36+
</div>
37+
);
38+
};
39+
40+
export default NotFound;

frontend/src/styles.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,39 @@ body {
926926
min-height: 32px;
927927
}
928928

929+
.not-found-button {
930+
display: inline-block;
931+
padding: var(--animated-button-padding-y) var(--animated-button-padding-x);
932+
background-color: var(--palette-main-brand-green);
933+
color: var(--palette-accent-white);
934+
text-decoration: none;
935+
border-radius: var(--border-radius-pill);
936+
font-size: var(--animated-button-font-size);
937+
transition:
938+
background-color 0.3s ease-in-out,
939+
transform 0.3s ease-in-out;
940+
}
941+
942+
.not-found-button:hover {
943+
background-color: var(--palette-dark-contrast-green);
944+
transform: translateY(-2px);
945+
box-shadow: var(--shadow-md);
946+
}
947+
948+
.dev-indicator {
949+
position: fixed;
950+
bottom: var(--spacing-sm);
951+
left: var(--spacing-sm);
952+
background: rgba(112, 214, 163, 0.2);
953+
color: var(--palette-dark-contrast-green);
954+
border: var(--border-width-thin) solid var(--palette-main-brand-green);
955+
padding: var(--spacing-xs) var(--spacing-sm);
956+
border-radius: var(--border-radius-sm);
957+
font-size: var(--font-size-sm);
958+
font-weight: var(--font-weight-semibold);
959+
z-index: 1000;
960+
}
961+
929962
@media (min-width: 768px) {
930963
.header-banner-content {
931964
align-items: center;

frontend/src/utils/devLogger.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Logs a message to the console only when the application is running in development mode.
3+
*
4+
* The 'import.meta.env.DEV' variable is automatically provided by Vite.
5+
* It is 'true' when Vite is running in development mode (e.g., via 'npm run dev').
6+
* It is 'false' when the application is built for production (e.g., via 'npm run build').
7+
*
8+
* @param {string} message - The message string or object to log.
9+
* @param {...any} optionalParams - Additional parameters to include in the log.
10+
*/
11+
export function devLog(message, ...optionalParams) {
12+
if (import.meta.env.DEV) {
13+
console.log('[DEV LOG]', message, ...optionalParams);
14+
}
15+
}

0 commit comments

Comments
 (0)