├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.tsx ├── axiosConfig.ts ├── components │ ├── Dashboard.tsx │ ├── Security.tsx │ ├── SidePanel.tsx │ ├── SignIn.tsx │ ├── SignUp.tsx │ ├── UrlMappingDetails.tsx │ ├── UrlShortener.tsx │ ├── UserAccount.tsx │ └── UserUrlMappings.tsx ├── context │ └── AuthContext.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts └── services │ └── AuthService.ts ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shorty-url-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-oauth/google": "^0.12.1", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.108", 12 | "@types/react": "^18.3.9", 13 | "@types/react-dom": "^18.3.0", 14 | "axios": "^1.7.7", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-icons": "^5.3.0", 18 | "react-router-dom": "^6.27.0", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "autoprefixer": "^10.4.20", 49 | "postcss": "^8.4.47", 50 | "tailwindcss": "^3.4.13" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Shorty URL 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | // src/App.tsx 2 | import React, { useContext } from 'react'; 3 | import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom'; 4 | import UrlShortener from './components/UrlShortener'; 5 | import SignIn from './components/SignIn'; 6 | import SignUp from './components/SignUp'; 7 | import UserAccount from './components/UserAccount'; 8 | import UserUrlMappings from './components/UserUrlMappings'; 9 | import UrlMappingDetails from './components/UrlMappingDetails'; 10 | import { AuthContext } from './context/AuthContext'; 11 | import { FaUserCircle } from 'react-icons/fa'; 12 | import Security from './components/Security'; 13 | import Dashboard from './components/Dashboard'; 14 | 15 | const App: React.FC = () => { 16 | const { isAuthenticated } = useContext(AuthContext); 17 | 18 | return ( 19 | 20 |
21 |
22 |
23 | 24 | Shorty URL 25 | 26 |
27 | {isAuthenticated ? ( 28 | 29 | 30 | 31 | ) : ( 32 | 36 | Sign In 37 | 38 | )} 39 |
40 |
41 |
42 | 43 |
44 | 45 | } /> 46 | } /> 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | } /> 52 | } /> 53 | } /> 54 | 55 |
56 | 57 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/axiosConfig.ts: -------------------------------------------------------------------------------- 1 | // src/axiosConfig.ts 2 | import axios from 'axios'; 3 | import AuthService from './services/AuthService'; 4 | 5 | const backendRestApiUrl = process.env.REACT_APP_BACKEND_REST_API_URL; 6 | 7 | // Create an instance of axios 8 | const axiosInstance = axios.create({ 9 | baseURL: backendRestApiUrl, 10 | }); 11 | 12 | // Request interceptor to add access token to headers 13 | axiosInstance.interceptors.request.use( 14 | (config) => { 15 | const accessToken = localStorage.getItem('accessToken'); 16 | if (accessToken && config.headers) { 17 | config.headers['Authorization'] = `Bearer ${accessToken}`; 18 | } 19 | return config; 20 | }, 21 | (error) => Promise.reject(error) 22 | ); 23 | 24 | // Response interceptor to handle token refresh 25 | axiosInstance.interceptors.response.use( 26 | (response) => response, 27 | async (error) => { 28 | const originalRequest = error.config; 29 | const refreshToken = localStorage.getItem('refreshToken'); 30 | 31 | if ( 32 | error.response && 33 | error.response.status === 403 && 34 | !originalRequest._retry && 35 | refreshToken 36 | ) { 37 | originalRequest._retry = true; 38 | try { 39 | // Use a new axios instance without interceptors to avoid infinite loops 40 | const response = await axios.create().post(`${backendRestApiUrl}/api/v1/auth/refresh-token`, { 41 | refreshToken, 42 | }); 43 | const newAccessToken = response.data.accessToken; 44 | localStorage.setItem('accessToken', newAccessToken); 45 | 46 | // Update the Authorization header for the original request 47 | originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; 48 | 49 | // Retry the original request with the new access token 50 | return axiosInstance(originalRequest); 51 | } catch (refreshError) { 52 | // Refresh token failed, log out the user 53 | AuthService.logout(); 54 | 55 | // Redirect to main page 56 | window.location.href = '/'; 57 | return Promise.reject(refreshError); 58 | } 59 | } 60 | return Promise.reject(error); 61 | } 62 | ); 63 | 64 | export default axiosInstance; 65 | -------------------------------------------------------------------------------- /src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SidePanel from './SidePanel'; 3 | import { FaLink, FaChartLine, FaUser, FaQrcode } from 'react-icons/fa'; 4 | 5 | const Dashboard: React.FC = () => { 6 | const metrics = { 7 | shortLinksCount: 42, 8 | totalVisitsCount: 1234, 9 | uniqueVisitsCount: 987, 10 | qrScans: 150, 11 | }; 12 | 13 | return ( 14 |
15 | 16 |
17 |

Dashboard

18 |
19 |
20 | 21 |
22 |

Total Short Links Count

23 |

{metrics.shortLinksCount}

24 |
25 |
26 |
27 | 28 |
29 |

Total Visits Count

30 |

{metrics.totalVisitsCount}

31 |
32 |
33 |
34 | 35 |
36 |

Total Unique Visits Count

37 |

{metrics.uniqueVisitsCount}

38 |
39 |
40 |
41 | 42 |
43 |

Total QR Scans

44 |

{metrics.qrScans}

45 |
46 |
47 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export default Dashboard; 54 | -------------------------------------------------------------------------------- /src/components/Security.tsx: -------------------------------------------------------------------------------- 1 | // src/components/Security.tsx 2 | import React, { useState } from 'react'; 3 | import axios from '../axiosConfig'; 4 | import SidePanel from './SidePanel'; 5 | 6 | const Security: React.FC = () => { 7 | const [currentPassword, setCurrentPassword] = useState(''); 8 | const [newPassword, setNewPassword] = useState(''); 9 | const [confirmPassword, setConfirmPassword] = useState(''); 10 | const [successMessage, setSuccessMessage] = useState(''); 11 | const [errorMessage, setErrorMessage] = useState(''); 12 | 13 | const handlePasswordChange = async (e: React.FormEvent) => { 14 | e.preventDefault(); 15 | 16 | if (newPassword !== confirmPassword) { 17 | setErrorMessage('New password and confirm password do not match.'); 18 | return; 19 | } 20 | 21 | try { 22 | await axios.put('/api/v1/users/change-password', { 23 | currentPassword, 24 | newPassword, 25 | }); 26 | 27 | setSuccessMessage('Password changed successfully.'); 28 | setErrorMessage(''); 29 | setCurrentPassword(''); 30 | setNewPassword(''); 31 | setConfirmPassword(''); 32 | } catch (error: any) { 33 | if (error.response) { 34 | setErrorMessage(error.response.data.errorMessage || 'Error changing password.'); 35 | } else if (error.request) { 36 | setErrorMessage('No response from the server. Please try again later.'); 37 | } else { 38 | setErrorMessage('Error: ' + error.message); 39 | } 40 | } 41 | }; 42 | 43 | return ( 44 |
45 | 46 |
47 |

Security

48 |
49 |
50 | 53 | setCurrentPassword(e.target.value)} 57 | required 58 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm" 59 | /> 60 |
61 |
62 | 65 | setNewPassword(e.target.value)} 69 | required 70 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm" 71 | /> 72 |
73 |
74 | 77 | setConfirmPassword(e.target.value)} 81 | required 82 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm" 83 | /> 84 |
85 | {errorMessage &&

{errorMessage}

} 86 | {successMessage &&

{successMessage}

} 87 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Security; 100 | -------------------------------------------------------------------------------- /src/components/SidePanel.tsx: -------------------------------------------------------------------------------- 1 | // src/components/SidePanel.tsx 2 | import React, { useState } from 'react'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import AuthService from '../services/AuthService'; 5 | import { 6 | FaBars, 7 | FaTimes, 8 | FaTachometerAlt, 9 | FaUser, 10 | FaLock, 11 | FaLink, 12 | FaSignOutAlt, 13 | } from 'react-icons/fa'; 14 | 15 | const SidePanel: React.FC = () => { 16 | const [isOpen, setIsOpen] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | const handleLogout = () => { 20 | AuthService.logout(); 21 | navigate('/'); 22 | }; 23 | 24 | const toggleMenu = () => { 25 | setIsOpen(!isOpen); 26 | }; 27 | 28 | const menuItems = ( 29 | 59 | ); 60 | 61 | return ( 62 | <> 63 | {/* Mobile Header */} 64 |
65 | 68 | Menu 69 |
70 | 71 | {/* Side Panel */} 72 |
77 |
78 |
{menuItems}
79 |
80 | 86 |
87 |
88 |
89 | 90 | ); 91 | }; 92 | 93 | export default SidePanel; 94 | -------------------------------------------------------------------------------- /src/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | // src/components/SignIn.tsx 2 | import React, { useState, useContext } from 'react'; 3 | import axios from '../axiosConfig'; 4 | import { useNavigate, Link } from 'react-router-dom'; 5 | import { AuthContext } from '../context/AuthContext'; 6 | 7 | const SignIn: React.FC = () => { 8 | const [email, setEmail] = useState(''); 9 | const [password, setPassword] = useState(''); 10 | const [errorMessage, setErrorMessage] = useState(''); 11 | const navigate = useNavigate(); 12 | 13 | const { setIsAuthenticated } = useContext(AuthContext); 14 | 15 | const handleSignIn = async (e: React.FormEvent) => { 16 | e.preventDefault(); 17 | 18 | try { 19 | const response = await axios.post('/api/v1/auth/signin', { 20 | email, 21 | password, 22 | }); 23 | 24 | localStorage.setItem('accessToken', response.data.accessToken); 25 | localStorage.setItem('refreshToken', response.data.refreshToken); 26 | 27 | setIsAuthenticated(true); 28 | setErrorMessage(''); 29 | navigate('/'); 30 | } catch (error: any) { 31 | if (error.response) { 32 | setErrorMessage(error.response.data.errorMessage || 'Error signing in.'); 33 | } else if (error.request) { 34 | setErrorMessage('No response from the server. Please try again later.'); 35 | } else { 36 | setErrorMessage('Error: ' + error.message); 37 | } 38 | } 39 | }; 40 | 41 | return ( 42 |
43 |

Sign In

44 |
45 | setEmail(e.target.value)} 50 | required 51 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm" 52 | /> 53 | setPassword(e.target.value)} 58 | required 59 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline mt-4 text-sm" 60 | /> 61 | 67 |
68 |
69 | Don't have an account?{' '} 70 | 71 | Sign Up 72 | 73 |
74 | {errorMessage &&

{errorMessage}

} 75 |
76 | ); 77 | }; 78 | 79 | export default SignIn; 80 | -------------------------------------------------------------------------------- /src/components/SignUp.tsx: -------------------------------------------------------------------------------- 1 | // src/components/SignUp.tsx 2 | import React, { useState, useContext } from 'react'; 3 | import axios from '../axiosConfig'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { AuthContext } from '../context/AuthContext'; 6 | 7 | const SignUp: React.FC = () => { 8 | const [formData, setFormData] = useState({ 9 | firstName: '', 10 | lastName: '', 11 | email: '', 12 | password: '', 13 | confirmPassword: '', 14 | country: '', 15 | age: '', 16 | }); 17 | const [errorMessage, setErrorMessage] = useState(''); 18 | const navigate = useNavigate(); 19 | 20 | const { setIsAuthenticated } = useContext(AuthContext); 21 | 22 | const handleChange = (e: React.ChangeEvent) => { 23 | setFormData({ ...formData, [e.target.name]: e.target.value }); 24 | }; 25 | 26 | const handleSignUp = async (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | 29 | if (formData.password !== formData.confirmPassword) { 30 | setErrorMessage('Passwords do not match.'); 31 | return; 32 | } 33 | 34 | try { 35 | const response = await axios.post('/api/v1/auth/signup', { 36 | firstName: formData.firstName, 37 | lastName: formData.lastName, 38 | email: formData.email, 39 | password: formData.password, 40 | country: formData.country, 41 | age: parseInt(formData.age, 10), 42 | }); 43 | 44 | localStorage.setItem('accessToken', response.data.accessToken); 45 | localStorage.setItem('refreshToken', response.data.refreshToken); 46 | 47 | setIsAuthenticated(true); 48 | setErrorMessage(''); 49 | navigate('/'); 50 | } catch (error: any) { 51 | if (error.response) { 52 | setErrorMessage(error.response.data.errorMessage || 'Error signing up.'); 53 | } else if (error.request) { 54 | setErrorMessage('No response from the server. Please try again later.'); 55 | } else { 56 | setErrorMessage('Error: ' + error.message); 57 | } 58 | } 59 | }; 60 | 61 | return ( 62 |
63 |

Sign Up

64 |
65 | 74 | 83 | 92 | 101 | 110 | 119 | 128 | 134 |
135 |

136 | By signing up, you agree to Shorty URL's{' '} 137 | 143 | terms of service 144 | {' '} 145 | and{' '} 146 | 152 | privacy policy 153 | 154 | . 155 |

156 | {errorMessage &&

{errorMessage}

} 157 |
158 | ); 159 | }; 160 | 161 | export default SignUp; 162 | -------------------------------------------------------------------------------- /src/components/UrlMappingDetails.tsx: -------------------------------------------------------------------------------- 1 | // src/components/UrlMappingDetails.tsx 2 | import React, { useEffect, useState } from 'react'; 3 | import axios from '../axiosConfig'; 4 | import { useNavigate, useParams } from 'react-router-dom'; 5 | import SidePanel from './SidePanel'; 6 | 7 | interface UrlMapping { 8 | urlHash: string; 9 | shortUrl: string; 10 | originalUrl: string; 11 | createdAt: string; 12 | expirationDate: string; 13 | } 14 | 15 | const UrlMappingDetails: React.FC = () => { 16 | const { urlHash } = useParams<{ urlHash: string }>(); 17 | const [urlMapping, setUrlMapping] = useState(null); 18 | const [errorMessage, setErrorMessage] = useState(''); 19 | const navigate = useNavigate(); 20 | 21 | useEffect(() => { 22 | const fetchUrlMapping = async () => { 23 | try { 24 | const response = await axios.get(`/api/v1/urls/${urlHash}`); 25 | setUrlMapping(response.data); 26 | } catch (error: any) { 27 | if (error.response && error.response.status === 401) { 28 | navigate('/signin'); 29 | } else { 30 | setErrorMessage('Failed to fetch URL mapping details.'); 31 | } 32 | } 33 | }; 34 | 35 | fetchUrlMapping(); 36 | }, [navigate, urlHash]); 37 | 38 | if (!urlMapping) { 39 | return

Loading URL mapping details...

; 40 | } 41 | 42 | return ( 43 |
44 | 45 |
46 |

URL Mapping Details

47 | {errorMessage &&

{errorMessage}

} 48 |
49 |

50 | Short URL:{' '} 51 | 52 | {urlMapping.shortUrl} 53 | 54 |

55 |

56 | Original URL:{' '} 57 | 58 | {urlMapping.originalUrl} 59 | 60 |

61 |

62 | Created At:{' '} 63 | {new Date(urlMapping.createdAt).toLocaleString()} 64 |

65 |

66 | Expiration Date:{' '} 67 | {new Date(urlMapping.expirationDate).toLocaleString()} 68 |

69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default UrlMappingDetails; 76 | -------------------------------------------------------------------------------- /src/components/UrlShortener.tsx: -------------------------------------------------------------------------------- 1 | // src/components/UrlShortener.tsx 2 | import React, { useState } from 'react'; 3 | import axios from '../axiosConfig'; 4 | 5 | const UrlShortener: React.FC = () => { 6 | const [originalUrl, setOriginalUrl] = useState(''); 7 | const [shortUrl, setShortUrl] = useState(''); 8 | const [errorMessage, setErrorMessage] = useState(''); 9 | 10 | const handleUrlSubmit = async (e: React.FormEvent) => { 11 | e.preventDefault(); 12 | 13 | try { 14 | const response = await axios.post('/api/v1/urls', { originalUrl }); 15 | setShortUrl(response.data.shortUrl); 16 | setErrorMessage(''); 17 | } catch (error: any) { 18 | if (error.response) { 19 | setErrorMessage(error.response.data.errorMessage || 'Error shortening the URL.'); 20 | } else if (error.request) { 21 | setErrorMessage('No response from the server. Please try again later.'); 22 | } else { 23 | setErrorMessage('Error: ' + error.message); 24 | } 25 | } 26 | }; 27 | 28 | const handleClear = () => { 29 | setOriginalUrl(''); 30 | setShortUrl(''); 31 | setErrorMessage(''); 32 | }; 33 | 34 | return ( 35 |
36 |

Create Short URL

37 |
38 | setOriginalUrl(e.target.value)} 43 | required 44 | className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline text-sm" 45 | /> 46 |
47 | 53 | 60 |
61 |
62 | 63 | {errorMessage &&

{errorMessage}

} 64 | 65 | {shortUrl && ( 66 |
67 |

Short URL Created!

68 | 69 | {shortUrl} 70 | 71 |
72 | )} 73 |
74 | ); 75 | }; 76 | 77 | export default UrlShortener; 78 | -------------------------------------------------------------------------------- /src/components/UserAccount.tsx: -------------------------------------------------------------------------------- 1 | // src/components/UserAccount.tsx 2 | import React, { useEffect, useState } from 'react'; 3 | import axios from '../axiosConfig'; 4 | import SidePanel from './SidePanel'; 5 | import { FaEdit } from 'react-icons/fa'; 6 | 7 | interface UserDetails { 8 | firstName: string; 9 | lastName: string; 10 | email: string; 11 | country: string; 12 | age: number; 13 | } 14 | 15 | const UserAccount: React.FC = () => { 16 | const [userDetails, setUserDetails] = useState(null); 17 | const [errorMessage, setErrorMessage] = useState(''); 18 | 19 | useEffect(() => { 20 | const fetchUserDetails = async () => { 21 | try { 22 | const response = await axios.get('/api/v1/users'); 23 | setUserDetails(response.data); 24 | } catch (error: any) { 25 | setErrorMessage('Failed to fetch user details.'); 26 | } 27 | }; 28 | 29 | fetchUserDetails(); 30 | }, []); 31 | 32 | const handleEditProfile = () => { 33 | alert('Edit profile functionality is not implemented yet.'); 34 | }; 35 | 36 | if (!userDetails) { 37 | return ( 38 |
39 |

Loading user details...

40 |
41 | ); 42 | } 43 | 44 | return ( 45 |
46 | 47 |
48 |
49 |

Profile

50 | {errorMessage && ( 51 |

{errorMessage}

52 | )} 53 |
54 |
55 |
56 | 57 |

{userDetails.firstName}

58 |
59 |
60 | 61 |

{userDetails.lastName}

62 |
63 |
64 | 65 |

{userDetails.email}

66 |
67 |
68 | 69 |

{userDetails.country}

70 |
71 |
72 | 73 |

{userDetails.age}

74 |
75 |
76 |
77 | 83 |
84 |
85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default UserAccount; 92 | -------------------------------------------------------------------------------- /src/components/UserUrlMappings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import axios from '../axiosConfig'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import SidePanel from './SidePanel'; 5 | import { 6 | FaTrash, 7 | FaInfoCircle, 8 | FaArrowLeft, 9 | FaArrowRight, 10 | } from 'react-icons/fa'; 11 | 12 | interface UrlMapping { 13 | urlHash: string; 14 | shortUrl: string; 15 | originalUrl: string; 16 | createdAt: string; 17 | expirationDate: string; 18 | } 19 | 20 | interface UrlMappingPage { 21 | content: UrlMapping[]; 22 | page: number; 23 | size: number; 24 | totalElements: number; 25 | totalPages: number; 26 | } 27 | 28 | const UserUrlMappings: React.FC = () => { 29 | const [urlMappings, setUrlMappings] = useState([]); 30 | const [page, setPage] = useState(0); 31 | const [size] = useState(10); 32 | const [totalPages, setTotalPages] = useState(0); 33 | const [errorMessage, setErrorMessage] = useState(''); 34 | const navigate = useNavigate(); 35 | 36 | const fetchUrlMappings = async (pageNumber: number) => { 37 | try { 38 | const response = await axios.get( 39 | `/api/v1/urls?page=${pageNumber}&size=${size}` 40 | ); 41 | const data: UrlMappingPage = response.data; 42 | setUrlMappings(data.content); 43 | setPage(data.page); 44 | setTotalPages(data.totalPages); 45 | } catch (error: any) { 46 | if (error.response && error.response.status === 401) { 47 | navigate('/signin'); 48 | } else { 49 | setErrorMessage('Failed to fetch URL mappings.'); 50 | } 51 | } 52 | }; 53 | 54 | useEffect(() => { 55 | fetchUrlMappings(page); 56 | }, [page]); 57 | 58 | const handleDelete = async (urlHash: string) => { 59 | const confirmDelete = window.confirm( 60 | 'Are you sure you want to delete this URL mapping?' 61 | ); 62 | if (!confirmDelete) return; 63 | 64 | try { 65 | await axios.delete(`/api/v1/urls/${urlHash}`); 66 | setUrlMappings( 67 | urlMappings.filter((mapping) => mapping.urlHash !== urlHash) 68 | ); 69 | } catch (error: any) { 70 | setErrorMessage('Failed to delete URL mapping.'); 71 | } 72 | }; 73 | 74 | const handlePreviousPage = () => { 75 | if (page > 0) { 76 | setPage(page - 1); 77 | } 78 | }; 79 | 80 | const handleNextPage = () => { 81 | if (page < totalPages - 1) { 82 | setPage(page + 1); 83 | } 84 | }; 85 | 86 | return ( 87 |
88 | 89 |
90 |

91 | My URL Mappings 92 |

93 | {errorMessage && ( 94 |

{errorMessage}

95 | )} 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {urlMappings.length > 0 ? ( 108 | urlMappings.map((mapping, index) => ( 109 | 110 | 113 | 123 | 133 | 149 | 150 | )) 151 | ) : ( 152 | 153 | 159 | 160 | )} 161 | 162 |
#Short URLOriginal URLActions
111 | {index + 1 + page * size} 112 | 114 | 120 | {mapping.shortUrl} 121 | 122 | 124 | 130 | {mapping.originalUrl} 131 | 132 | 134 | 142 | 148 |
157 | No URL mappings found. 158 |
163 |
164 |
165 | 176 | 187 |
188 |
189 |
190 | ); 191 | }; 192 | 193 | export default UserUrlMappings; 194 | -------------------------------------------------------------------------------- /src/context/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | // src/context/AuthContext.tsx 2 | import React, { createContext, useState, useEffect } from 'react'; 3 | import AuthService from '../services/AuthService'; 4 | 5 | interface AuthContextType { 6 | isAuthenticated: boolean; 7 | setIsAuthenticated: (value: boolean) => void; 8 | } 9 | 10 | export const AuthContext = createContext({ 11 | isAuthenticated: false, 12 | setIsAuthenticated: () => {}, 13 | }); 14 | 15 | export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 16 | const [isAuthenticated, setIsAuthenticated] = useState(AuthService.isAuthenticated); 17 | 18 | useEffect(() => { 19 | const handleAuthChange = () => { 20 | setIsAuthenticated(AuthService.isAuthenticated); 21 | }; 22 | 23 | AuthService.addListener(handleAuthChange); 24 | 25 | return () => { 26 | AuthService.removeListener(handleAuthChange); 27 | }; 28 | }, []); 29 | 30 | return ( 31 | 37 | {children} 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* src/index.css */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | html, body { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | font-family: 'Inter', sans-serif; 13 | } 14 | 15 | main { 16 | flex-grow: 1; 17 | } 18 | 19 | img { 20 | max-height: 40px; 21 | } 22 | 23 | .footer { 24 | background-color: #1f2937; 25 | color: white; 26 | padding: 20px; 27 | text-align: center; 28 | } 29 | 30 | a { 31 | text-decoration: none; 32 | } 33 | 34 | input:focus { 35 | outline: none; 36 | } 37 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // src/index.tsx 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import './index.css'; 5 | import App from './App'; 6 | import { AuthProvider } from './context/AuthContext'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/services/AuthService.ts: -------------------------------------------------------------------------------- 1 | // src/services/AuthService.ts 2 | class AuthService { 3 | private static instance: AuthService; 4 | public isAuthenticated: boolean = !!localStorage.getItem('accessToken'); 5 | private listeners: Array<() => void> = []; 6 | 7 | private constructor() {} 8 | 9 | public static getInstance(): AuthService { 10 | if (!AuthService.instance) { 11 | AuthService.instance = new AuthService(); 12 | } 13 | return AuthService.instance; 14 | } 15 | 16 | public setAuthenticated(value: boolean) { 17 | this.isAuthenticated = value; 18 | this.notifyListeners(); 19 | } 20 | 21 | public logout() { 22 | localStorage.removeItem('accessToken'); 23 | localStorage.removeItem('refreshToken'); 24 | this.isAuthenticated = false; 25 | this.notifyListeners(); 26 | } 27 | 28 | public addListener(listener: () => void) { 29 | this.listeners.push(listener); 30 | } 31 | 32 | public removeListener(listener: () => void) { 33 | this.listeners = this.listeners.filter((l) => l !== listener); 34 | } 35 | 36 | private notifyListeners() { 37 | this.listeners.forEach((listener) => listener()); 38 | } 39 | } 40 | 41 | export default AuthService.getInstance(); 42 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------