mirror of
https://github.com/20kaushik02/portfolio-website.git
synced 2026-09-10 14:04:09 +00:00
Add URL-based deep linking and a vertical timeline for Career
Co-authored-by: 20kaushik02 <59662438+20kaushik02@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6717cbb30e
commit
64d611d9c4
58
package-lock.json
generated
58
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@mui/material": "^7.3.7",
|
"@mui/material": "^7.3.7",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
"react-router-dom": "^7.18.2",
|
||||||
"swiper": "^12.0.3"
|
"swiper": "^12.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -14996,6 +14997,57 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.18.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router/node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-scripts": {
|
"node_modules/react-scripts": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||||
@@ -15950,6 +16002,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"@mui/material": "^7.3.7",
|
"@mui/material": "^7.3.7",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
"react-router-dom": "^7.18.2",
|
||||||
"swiper": "^12.0.3"
|
"swiper": "^12.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import Menu from './components/Menu';
|
import Menu from './components/Menu';
|
||||||
@@ -57,7 +58,9 @@ function App() {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<WidthContext.Provider value={width}>
|
<WidthContext.Provider value={width}>
|
||||||
<Menu />
|
<BrowserRouter basename={process.env.PUBLIC_URL || '/'}>
|
||||||
|
<Menu />
|
||||||
|
</BrowserRouter>
|
||||||
</WidthContext.Provider>
|
</WidthContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ColorModeContext.Provider>
|
</ColorModeContext.Provider>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Divider, List, ListItem, ListItemText, Typography } from '@mui/material'
|
import { Box, List, ListItem, ListItemText, Typography, useTheme } from '@mui/material'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
export const experiencesData = [
|
export const experiencesData = [
|
||||||
@@ -62,68 +62,66 @@ export const experiencesData = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Simple, dependency-free vertical timeline: a dot + connecting line down the
|
||||||
|
// left edge, with each entry's content to the right.
|
||||||
|
const TimelineEntry = ({ exp, isLast }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 24, flexShrink: 0 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: exp.endDate === '' ? theme.palette.secondary.main : theme.palette.primary.main,
|
||||||
|
border: `2px solid ${theme.palette.background.paper}`,
|
||||||
|
boxShadow: `0 0 0 2px ${theme.palette.primary.main}`,
|
||||||
|
mt: '6px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!isLast && (
|
||||||
|
<Box sx={{ flexGrow: 1, width: '2px', bgcolor: theme.palette.divider, my: '2px' }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flexGrow: 1, pb: isLast ? 1 : 3 }}>
|
||||||
|
<Typography px={2} align="left" fontSize={16}>
|
||||||
|
<b>{exp.designation}, {exp.location}</b>, <i>{exp.startDate} - {exp.endDate || 'Present'}</i>
|
||||||
|
</Typography>
|
||||||
|
<Typography component="div" p={2} align="left">
|
||||||
|
<List disablePadding sx={{ pl: 2, listStyleType: 'disc' }}>
|
||||||
|
{exp.desc.map((line, index) =>
|
||||||
|
<ListItem disablePadding key={index} sx={{ display: 'list-item' }}>
|
||||||
|
<ListItemText slotProps={{ primary: { fontSize: 13 } }} primary={line} />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimelineSection = ({ title, entries }) => (
|
||||||
|
<Fragment>
|
||||||
|
<Typography p={2} align="left" variant="h5">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
{entries.map((exp, idx) => (
|
||||||
|
<TimelineEntry key={exp.key} exp={exp} isLast={idx === entries.length - 1} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
const Career = () => {
|
const Career = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography p={2} align="left" variant="h5">
|
<TimelineSection title="Professional" entries={experiencesData.filter(exp => exp.type === 'prof')} />
|
||||||
Professional
|
<TimelineSection title="Education" entries={experiencesData.filter(exp => exp.type === 'edu')} />
|
||||||
</Typography>
|
<TimelineSection title="Volunteering" entries={experiencesData.filter(exp => exp.type === 'extra')} />
|
||||||
{experiencesData.filter(exp => exp.type === 'prof').map((exp, idx) =>
|
|
||||||
<Fragment key={idx}>
|
|
||||||
<Typography px={2} align="left" fontSize={16}>
|
|
||||||
<b>{exp.designation}, {exp.location}</b>, <i>{exp.startDate} - {exp.endDate}</i>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" p={2} align="left">
|
|
||||||
<List disablePadding sx={{ pl: 2, listStyleType: 'disc' }}>
|
|
||||||
{exp.desc.map((line, index) =>
|
|
||||||
<ListItem disablePadding key={index} sx={{ display: 'list-item' }}>
|
|
||||||
<ListItemText slotProps={{ primary: { fontSize: 13 } }} primary={line} />
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Typography>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<Typography p={2} align="left" variant="h5">
|
|
||||||
Education
|
|
||||||
</Typography>
|
|
||||||
{experiencesData.filter(exp => exp.type === 'edu').map((exp, idx) =>
|
|
||||||
<Fragment key={idx}>
|
|
||||||
<Typography px={2} align="left" fontSize={16}>
|
|
||||||
<b>{exp.designation}, {exp.location}</b>, <i>{exp.startDate} - {exp.endDate}</i>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" p={2} align="left">
|
|
||||||
<List disablePadding sx={{ pl: 2, listStyleType: 'disc' }}>
|
|
||||||
{exp.desc.map((line, index) =>
|
|
||||||
<ListItem disablePadding key={index} sx={{ display: 'list-item' }}>
|
|
||||||
<ListItemText slotProps={{ primary: { fontSize: 13 } }} primary={line} />
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Typography>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<Typography p={2} align="left" variant="h5">
|
|
||||||
Volunteering
|
|
||||||
</Typography>
|
|
||||||
{experiencesData.filter(exp => exp.type === 'extra').map((exp, idx) =>
|
|
||||||
<Fragment key={idx}>
|
|
||||||
<Typography px={2} align="left" fontSize={16}>
|
|
||||||
<b>{exp.designation}, {exp.location}</b>, <i>{exp.startDate} - {exp.endDate}</i>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" p={2} align="left">
|
|
||||||
<List disablePadding sx={{ pl: 2, listStyleType: 'disc' }}>
|
|
||||||
{exp.desc.map((line, index) =>
|
|
||||||
<ListItem disablePadding key={index} sx={{ display: 'list-item' }}>
|
|
||||||
<ListItemText slotProps={{ primary: { fontSize: 13 } }} primary={line} />
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Typography>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Route, Routes, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import AppBar from '@mui/material/AppBar';
|
import AppBar from '@mui/material/AppBar';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
@@ -38,19 +39,19 @@ const drawerWidth = 240;
|
|||||||
|
|
||||||
const menuSections = [
|
const menuSections = [
|
||||||
{
|
{
|
||||||
key: 'landing', display_name: 'About', appbar_text: 'Hello There!', display_icon: <LandingIcon />,
|
key: 'landing', path: '/', display_name: 'About', appbar_text: 'Hello There!', display_icon: <LandingIcon />,
|
||||||
extLink: false, component: <Landing />
|
extLink: false, component: <Landing />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'exp', display_name: 'Work', appbar_text: 'Journey', display_icon: <CareerIcon />,
|
key: 'exp', path: '/work', display_name: 'Work', appbar_text: 'Journey', display_icon: <CareerIcon />,
|
||||||
extLink: false, component: <Career />
|
extLink: false, component: <Career />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'proj', display_name: 'Stuff', appbar_text: 'Workbench', display_icon: <ProjectsIcon />,
|
key: 'proj', path: '/stuff', display_name: 'Stuff', appbar_text: 'Workbench', display_icon: <ProjectsIcon />,
|
||||||
extLink: false, component: <Projects />
|
extLink: false, component: <Projects />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'ints', display_name: 'Interests', appbar_text: 'My Interests', display_icon: <InterestsIcon />,
|
key: 'ints', path: '/interests', display_name: 'Interests', appbar_text: 'My Interests', display_icon: <InterestsIcon />,
|
||||||
extLink: false, component: <UnderConstruction />
|
extLink: false, component: <UnderConstruction />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,11 +64,17 @@ const menuSections = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const internalSections = menuSections.filter(menuItem => menuItem.extLink === false);
|
||||||
|
|
||||||
function Menu(props) {
|
function Menu(props) {
|
||||||
const [mobileOpen, setMobileOpen] = React.useState(false);
|
const [mobileOpen, setMobileOpen] = React.useState(false);
|
||||||
const [isClosing, setIsClosing] = React.useState(false);
|
const [isClosing, setIsClosing] = React.useState(false);
|
||||||
const [activeSection, setActiveSection] = React.useState(menuSections[0]);
|
|
||||||
const { mode, toggleMode } = React.useContext(ColorModeContext);
|
const { mode, toggleMode } = React.useContext(ColorModeContext);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const activeSection = internalSections.find((menuItem) => menuItem.path === location.pathname)
|
||||||
|
|| internalSections[0];
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
document.title = `Kaushik | ${activeSection.display_name}`;
|
document.title = `Kaushik | ${activeSection.display_name}`;
|
||||||
@@ -91,7 +98,7 @@ function Menu(props) {
|
|||||||
const handleDrawerSelect = (selectedSectionKey) => {
|
const handleDrawerSelect = (selectedSectionKey) => {
|
||||||
handleDrawerClose();
|
handleDrawerClose();
|
||||||
const menuSection = menuSections.filter((menuItem) => menuItem.key === selectedSectionKey)[0];
|
const menuSection = menuSections.filter((menuItem) => menuItem.key === selectedSectionKey)[0];
|
||||||
setActiveSection(menuSection);
|
navigate(menuSection.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawer = (
|
const drawer = (
|
||||||
@@ -203,7 +210,12 @@ function Menu(props) {
|
|||||||
sx={{ flexGrow: 1, padding: 1, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
|
sx={{ flexGrow: 1, padding: 1, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
|
||||||
>
|
>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
{activeSection.component}
|
<Routes>
|
||||||
|
{internalSections.map((menuItem) => (
|
||||||
|
<Route key={menuItem.key} path={menuItem.path} element={menuItem.component} />
|
||||||
|
))}
|
||||||
|
<Route path="*" element={<Navigate to={internalSections[0].path} replace />} />
|
||||||
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user