Compare commits

...

6 Commits

  1. 33
      package.json
  2. 11671
      pnpm-lock.yaml
  3. 5
      src/blog.tsx
  4. 211
      src/components/Container.tsx
  5. 38
      src/components/Menu.tsx
  6. 19
      src/components/Timeline.tsx
  7. 5
      src/index.css
  8. 48
      src/index.tsx
  9. 32
      src/pages/blog/Home.tsx
  10. 6
      src/pages/blog/components/BlogContent.tsx
  11. 10
      src/pages/main/404.tsx
  12. 39
      src/pages/main/Contact.tsx
  13. 103
      src/pages/main/Exp.tsx
  14. 2
      src/pages/main/Home.tsx
  15. 2
      src/pages/main/Projects.tsx
  16. 27
      src/util/index.ts
  17. 9
      vite.config.ts

33
package.json

@ -15,28 +15,25 @@
] ]
}, },
"dependencies": { "dependencies": {
"@emotion/css": "^11.9.0", "@emotion/css": "^11.11.2",
"classnames": "^2.3.1", "date-fns": "^2.30.0",
"date-fns": "^2.28.0", "framer-motion": "^10.16.4",
"framer-motion": "^6.3.15", "gm": "^1.25.0",
"gm": "^1.23.1",
"imagen": "github:MKRhere/imagen", "imagen": "github:MKRhere/imagen",
"marked": "^4.0.17", "marked": "^9.0.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.3.0" "wouter": "^2.11.0"
}, },
"devDependencies": { "devDependencies": {
"@svgr/rollup": "^6.2.1", "@svgr/rollup": "^8.1.0",
"@types/gm": "^1.18.12", "@types/gm": "^1.25.2",
"@types/marked": "^4.0.3", "@types/marked": "^5.0.2",
"@types/node": "^18.0.0", "@types/node": "^20.8.0",
"@types/react": "^18.0.14", "@types/react": "^18.2.24",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.2.8",
"@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.1.0",
"@vitejs/plugin-react": "^1.3.2", "typescript": "^5.2.2",
"react-scripts": "5.0.1", "vite": "^4.4.9"
"typescript": "^4.7.4",
"vite": "^2.9.12"
} }
} }

11671
pnpm-lock.yaml

File diff suppressed because it is too large

5
src/blog.tsx

@ -1,15 +1,12 @@
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import "./index.css"; import "./index.css";
import BlogHome from "./pages/blog/Home"; import BlogHome from "./pages/blog/Home";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<Router> <BlogHome />
<BlogHome />
</Router>
</React.StrictMode>, </React.StrictMode>,
); );

211
src/components/Container.tsx

@ -1,35 +1,30 @@
import React, { useState, useEffect, useRef, useLayoutEffect } from "react"; import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
import { css, cx } from "@emotion/css"; import { css, cx } from "@emotion/css";
import { useNavigate } from "react-router-dom"; import useLocation from "wouter/use-location";
import { ReactComponent as Logo } from "../assets/logo.svg"; import { ReactComponent as Logo } from "../assets/logo.svg";
import { ReactComponent as Right } from "../assets/arrow-right.svg"; import { ReactComponent as Right } from "../assets/arrow-right.svg";
import { getTimeout } from "../util"; import { get, getTimeout } from "../util";
import Menu from "./Menu"; import Menu, { MenuEntries } from "./Menu";
import useMediaQuery from "../util/useMediaQuery"; import useMediaQuery from "../util/useMediaQuery";
const [timer, clear] = getTimeout(); const [timer, clear] = getTimeout();
const Container: React.FC<{ const Container: React.FC<{
children: (string | React.DetailedReactHTMLElement<any, HTMLElement> | React.ReactElement)[]; children: (
| string
| React.DetailedReactHTMLElement<any, HTMLElement>
| React.ReactElement
)[];
hideNav?: boolean; hideNav?: boolean;
arrowReversed?: boolean;
next?: string;
className?: string; className?: string;
}> = ({ }> = ({ children: _children, hideNav = false, className, ...props }) => {
children: _children, const [location, navigate] = useLocation();
hideNav = false,
arrowReversed = false,
next,
className,
...props
}) => {
const navigate = useNavigate();
const mobile = useMediaQuery("(max-width: 50rem)"); const mobile = useMediaQuery("(max-width: 50rem)");
const logoContainer = useRef<HTMLButtonElement>(null); const logoContainer = useRef<HTMLButtonElement>(null);
const highlightCircle = useRef<HTMLSpanElement>(null); const highlightCircle = useRef<HTMLButtonElement>(null);
const containerChild = useRef<HTMLDivElement>(null); const containerChild = useRef<HTMLDivElement>(null);
const nextBtn = useRef<HTMLButtonElement>(null); const nextBtn = useRef<HTMLButtonElement>(null);
@ -37,11 +32,20 @@ const Container: React.FC<{
const children = React.Children.map( const children = React.Children.map(
_children, _children,
(child: string | React.DetailedReactHTMLElement<any, HTMLElement> | React.ReactElement) => (
child:
| string
| React.DetailedReactHTMLElement<any, HTMLElement>
| React.ReactElement,
) =>
!child || typeof child === "string" !child || typeof child === "string"
? child ? child
: React.cloneElement(child, { : React.cloneElement(child, {
style: { opacity: 0, transform: "translateY(3rem)", transition: "all 300ms" }, style: {
opacity: 0,
transform: "translateY(3rem)",
transition: "all 300ms",
},
}), }),
); );
@ -52,14 +56,19 @@ const Container: React.FC<{
if (highlightCircle.current) { if (highlightCircle.current) {
highlightCircle.current.classList.add("highlight"); highlightCircle.current.classList.add("highlight");
timer( timer(
() => highlightCircle.current && highlightCircle.current.classList.remove("highlight"), () =>
highlightCircle.current &&
highlightCircle.current.classList.remove("highlight"),
1500, 1500,
); );
} }
if (nextBtn.current) { if (nextBtn.current) {
nextBtn.current.style.width = "4rem"; nextBtn.current.style.width = "4rem";
timer(() => nextBtn.current && (nextBtn.current.style.right = "10vw"), 300); timer(
() => nextBtn.current && (nextBtn.current.style.right = "10vw"),
300,
);
} }
if (containerChild.current) { if (containerChild.current) {
@ -86,7 +95,9 @@ const Container: React.FC<{
const handleResize = () => { const handleResize = () => {
if (containerChild.current && logoContainer.current) if (containerChild.current && logoContainer.current)
logoContainer.current.style.left = `${containerChild.current.getBoundingClientRect().x}px`; logoContainer.current.style.left = `${
containerChild.current.getBoundingClientRect().x
}px`;
}; };
useEffect(() => { useEffect(() => {
@ -96,33 +107,92 @@ const Container: React.FC<{
return () => window.removeEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize);
}, []); }, []);
// on first render type MouseKb = React.MouseEvent | React.KeyboardEvent | KeyboardEvent;
useLayoutEffect(handleResize, []);
const handleNext: React.MouseEventHandler<HTMLButtonElement> = e => { const animateArrow = (e: MouseKb) => {
if (containerChild.current) { if (containerChild.current) {
([...containerChild.current.children] as (HTMLElement | SVGElement)[]).forEach(child => { (
[...containerChild.current.children] as (HTMLElement | SVGElement)[]
).forEach(child => {
child.style.marginBottom = "2rem"; child.style.marginBottom = "2rem";
child.style.opacity = "0"; child.style.opacity = "0";
}); });
} }
document.body.style.maxHeight = "100vh"; document.body.style.maxHeight = "100vh";
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
e.currentTarget.style.width = "0"; try {
const target = e.currentTarget! as HTMLButtonElement;
target.style.width = "0";
} catch {}
};
const current = MenuEntries.findIndex(([, path]) => location === path);
const next = get.next(MenuEntries, current)[1];
const prev = get.prev(MenuEntries, current)[1];
const end = current === MenuEntries.length - 1;
const handlePrev = (e: MouseKb) => {
animateArrow(e);
timer(() => prev && navigate(prev), 300);
};
const handleNext = (e: MouseKb) => {
animateArrow(e);
timer(() => next && navigate(next), 300); timer(() => next && navigate(next), 300);
}; };
function kbnav(e: KeyboardEvent) {
switch (e.key) {
case "ArrowLeft":
return handlePrev(e);
case "ArrowRight":
return handleNext(e);
}
}
useEffect(() => {
window.addEventListener("keydown", kbnav);
// cleanup
return () => window.removeEventListener("keydown", kbnav);
}, []);
// on first render
useLayoutEffect(handleResize, []);
return ( return (
<div <div
className={css` className={css`
background: var(--background-colour); background: var(--background-colour);
padding: 15rem calc(100vw / 8) 8rem calc(100vw / 8); padding-block-start: 15rem;
padding-block-end: 8rem;
padding-inline: calc(100vw / 8);
overflow-x: hidden; overflow-x: hidden;
min-height: 100vh; min-height: 100vh;
position: relative; position: relative;
`}> `}>
<div
className={cx(
"fog",
css`
position: fixed;
width: 100vw;
left: 0;
bottom: 0;
height: 10rem;
background: rgb(0, 0, 0);
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 100%
);
z-index: 1000;
pointer-events: none;
`,
)}
/>
{!hideNav && ( {!hideNav && (
<button <span
ref={logoContainer} ref={logoContainer}
className={css` className={css`
position: absolute; position: absolute;
@ -134,7 +204,7 @@ const Container: React.FC<{
`} `}
onMouseOver={() => !mobile && setShowMenu(true)} onMouseOver={() => !mobile && setShowMenu(true)}
onMouseOut={() => !mobile && setShowMenu(false)}> onMouseOut={() => !mobile && setShowMenu(false)}>
<span <button
ref={highlightCircle} ref={highlightCircle}
className={cx( className={cx(
css` css`
@ -143,7 +213,9 @@ const Container: React.FC<{
height: 5rem; height: 5rem;
width: 5rem; width: 5rem;
border-radius: 100%; border-radius: 100%;
box-shadow: 0px 0px 50px 0px rgba(100, 100, 100, 0.65); border: 0;
background: none;
box-shadow: 0 0 1rem 0 rgba(100, 100, 100, 0.5);
cursor: pointer; cursor: pointer;
& > svg { & > svg {
@ -152,11 +224,13 @@ const Container: React.FC<{
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 1; z-index: 1;
outline: 0;
} }
&::before { &::before {
content: ""; content: "";
position: absolute; position: absolute;
top: 0.5rem;
left: -0.1rem; left: -0.1rem;
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
@ -174,11 +248,13 @@ const Container: React.FC<{
opacity: 1; opacity: 1;
} }
&:hover::before { &:hover::before,
&:focus::before {
width: 5.2rem; width: 5.2rem;
height: 5.2rem; height: 5.2rem;
top: -0.05rem; top: -0.1rem;
left: -0.1rem; left: -0.1rem;
outline: none;
} }
&.highlight::before { &.highlight::before {
@ -193,49 +269,48 @@ const Container: React.FC<{
viewBox="0 0 264 264" viewBox="0 0 264 264"
onClick={() => (mobile ? setShowMenu(true) : navigate("/"))} onClick={() => (mobile ? setShowMenu(true) : navigate("/"))}
/> />
</span> </button>
<Menu show={showMenu} setShowMenu={setShowMenu} /> <Menu show={showMenu} setShowMenu={setShowMenu} />
</button> </span>
)} )}
{next && ( <button
<button onClick={handleNext}
onClick={handleNext} ref={nextBtn}
ref={nextBtn} title={end ? "Back to start" : "Next page"}
className={css`
position: fixed;
right: 14vw;
bottom: 10vh;
z-index: 500;
background: none;
padding: 0;
font-weight: 500;
cursor: pointer;
letter-spacing: 0.2rem;
border: none;
overflow: hidden;
width: 0;
transition: all 300ms;
overflow: hidden;
${end ? "rotate: 180deg;" : ""}
&:hover * {
fill: var(--primary-colour);
}
`}>
<Right
className={css` className={css`
position: fixed; height: "2rem";
right: 14vw; width: "2rem";
bottom: 10vh; `}
z-index: 500; />
background: none; </button>
padding: 0;
font-weight: 500;
cursor: pointer;
letter-spacing: 0.2rem;
border: none;
overflow: hidden;
width: 0;
transition: all 300ms;
overflow: hidden;
${arrowReversed ? "rotate: 180deg;" : ""}
&:hover * {
fill: var(--primary-colour);
}
`}>
<Right
className={css`
height: "2rem";
width: "2rem";
`}
/>
</button>
)}
<div <div
className={cx( className={cx(
css` css`
width: 100%; width: 100%;
max-width: 60rem; max-width: 62rem;
min-height: 100%; min-height: 100%;
margin: auto; margin: auto;

38
src/components/Menu.tsx

@ -1,16 +1,18 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom";
import { css, cx } from "@emotion/css"; import { css, cx } from "@emotion/css";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import RevealChildren from "./RevealChildren"; import RevealChildren from "./RevealChildren";
import useMediaQuery from "../util/useMediaQuery"; import useMediaQuery from "../util/useMediaQuery";
import { useNav } from "../util";
const menu = [ export const MENU = {
{ name: "Home", link: "/" }, Home: "/",
{ name: "Experience", link: "/experience" }, Experience: "/experience",
{ name: "Projects", link: "/projects" }, Projects: "/projects",
{ name: "Contact", link: "/contact" }, Contact: "/contact",
]; } as const;
export const MenuEntries = Object.entries(MENU);
const desktopNav = css` const desktopNav = css`
float: right; float: right;
@ -44,6 +46,11 @@ const menuList = css`
& > li { & > li {
margin-left: 1rem; margin-left: 1rem;
} }
& :focus-within {
opacity: 1 !important;
outline: none;
}
`; `;
const mobileMenu = css` const mobileMenu = css`
@ -57,18 +64,19 @@ const mobileMenu = css`
} }
`; `;
const Menu: React.FC<{ show?: boolean; setShowMenu: (show: boolean) => void }> = ({ const Menu: React.FC<{
show = false, show?: boolean;
setShowMenu, setShowMenu: (show: boolean) => void;
}) => { }> = ({ show = false, setShowMenu }) => {
const navigate = useNav();
// use same query as elsewhere for consistency // use same query as elsewhere for consistency
const mobile = useMediaQuery("(max-width: 50rem)"); const mobile = useMediaQuery("(max-width: 50rem)");
const notmobile = !mobile; const notmobile = !mobile;
const menuItems = menu.map(item => ( const menuItems = Object.entries(MENU).map(([name, link]) => (
<Link key={item.link} to={item.link}> <a key={link} onClick={navigate(link)} href={link}>
{item.name} {name}
</Link> </a>
)); ));
return ( return (

19
src/components/Timeline.tsx

@ -2,7 +2,12 @@ import React, { useMemo } from "react";
import { css, cx } from "@emotion/css"; import { css, cx } from "@emotion/css";
import { format, isBefore, startOfDay } from "date-fns"; import { format, isBefore, startOfDay } from "date-fns";
export type TimelineUnit = { title?: string; url?: string; img?: string; date: string }; export type TimelineUnit = {
title?: string;
url?: string;
img?: string;
date: string;
};
export type TimelineUnits = TimelineUnit[]; export type TimelineUnits = TimelineUnit[];
const unit = css` const unit = css`
@ -15,7 +20,7 @@ const unit = css`
font-size: inherit; font-size: inherit;
text-align: inherit; text-align: inherit;
font-weight: inherit; font-weight: inherit;
max-width: 400px; max-width: 20rem;
transition: 100ms all; transition: 100ms all;
text-decoration: none; text-decoration: none;
position: relative; position: relative;
@ -82,7 +87,11 @@ const tlcontainer = css`
bottom: 0; bottom: 0;
left: 0; left: 0;
background: rgb(0, 0, 0); background: rgb(0, 0, 0);
background: linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(
0deg,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0) 100%
);
} }
`; `;
@ -107,7 +116,9 @@ const Unit: React.FC<{ contents: TimelineUnit }> = ({ contents }) => {
`, `,
)} )}
onClick={() => onClick={() =>
contents.url ? window.open(contents.url, "_blank", `noreferrer, noopener`) : "" contents.url
? window.open(contents.url, "_blank", `noreferrer, noopener`)
: ""
}> }>
{contents.title ? <h4>{contents.title}</h4> : ""} {contents.title ? <h4>{contents.title}</h4> : ""}
<h3>{format(date, "h:mm a")}</h3> <h3>{format(date, "h:mm a")}</h3>

5
src/index.css

@ -5,7 +5,7 @@
--card-tags-hover: rgb(25, 25, 25); --card-tags-hover: rgb(25, 25, 25);
--primary-colour: rgb(255, 85, 85); --primary-colour: rgb(255, 85, 85);
--text-colour: rgb(211, 207, 201); --text-colour: rgb(211, 207, 201);
--text-subdued: rgb(163, 163, 163); --text-subdued: rgb(150, 150, 150);
font-weight: 500; font-weight: 500;
font-size: max(16px, 0.8vw); font-size: max(16px, 0.8vw);
} }
@ -77,6 +77,9 @@ h4 {
a { a {
color: var(--text-colour); color: var(--text-colour);
text-decoration: none; text-decoration: none;
font-family: Inter;
font-weight: 800;
transition: all 200ms;
} }
a:hover { a:hover {

48
src/index.tsx

@ -3,7 +3,7 @@ import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import useLocation from "wouter/use-location";
import Home from "./pages/main/Home"; import Home from "./pages/main/Home";
import Exp from "./pages/main/Exp"; import Exp from "./pages/main/Exp";
@ -13,22 +13,40 @@ import Live from "./pages/main/Live";
import NotFound from "./pages/main/404"; import NotFound from "./pages/main/404";
import BlogHome from "./pages/blog/Home"; import BlogHome from "./pages/blog/Home";
import { BlogPost } from "./pages/blog/components/BlogContent";
import { normalise } from "./util";
function App() {
const [location, navigate] = useLocation();
const normalised = normalise(location);
if (location !== normalised) {
navigate(normalised, { replace: true });
return null;
}
switch (normalised) {
case "/":
return <Home />;
case "/experience":
return <Exp />;
case "/projects":
return <Projects />;
case "/contact":
return <Contact />;
case "/live":
return <Live />;
case "/blog":
// return <BlogHome />;
default:
// if (location.startsWith("/blog")) return <BlogPost />;
return <NotFound />;
}
}
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<Router> <App />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/experience" element={<Exp />} />
<Route path="/projects" element={<Projects />} />
<Route path="/contact" element={<Contact />} />
<Route path="/live" element={<Live />} />
{/* <Route path="/blog" element={<BlogHome />} /> */}
{/* <Route path="/blog/*" element={<BlogHome />} /> */}
<Route path="/*" element={<NotFound />} />
</Routes>
</Router>
</React.StrictMode>, </React.StrictMode>,
); );

32
src/pages/blog/Home.tsx

@ -1,12 +1,12 @@
import { css } from "@emotion/css"; import { css, cx } from "@emotion/css";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom"; import useLocation from "wouter/use-location";
import { Spacer } from "../../components/Spacer"; import { Spacer } from "../../components/Spacer";
import { ArticleSubHeader } from "./components/ArticleSubHeader"; import { ArticleSubHeader } from "./components/ArticleSubHeader";
import { BlogPost } from "./components/BlogContent"; import { BlogPost } from "./components/BlogContent";
import { articles, getBlogPath } from "../../data"; import { articles, getBlogPath } from "../../data";
import { ReactComponent as DrawClose } from "../../assets/arrow-thin.svg"; import { ReactComponent as DrawClose } from "../../assets/arrow-thin.svg";
import classNames from "classnames"; import { useNav } from "../../util";
const Header: React.FC = () => { const Header: React.FC = () => {
return ( return (
@ -58,10 +58,10 @@ const Header: React.FC = () => {
}; };
const BlogHome: React.FC = () => { const BlogHome: React.FC = () => {
const location = useLocation(); const [location] = useLocation();
const navigate = useNavigate(); const navigate = useNav();
const isArticleOpen = Boolean(location.pathname.split("/blog")[1]); const isArticleOpen = Boolean(location.split("/blog")[1]);
const [isAsideClosed, setAsideClosed] = useState(isArticleOpen); const [isAsideClosed, setAsideClosed] = useState(isArticleOpen);
useEffect(() => { useEffect(() => {
@ -73,7 +73,7 @@ const BlogHome: React.FC = () => {
if (!isArticleOpen) return; if (!isArticleOpen) return;
const handler = (e: KeyboardEvent) => const handler = (e: KeyboardEvent) =>
e.key === "Escape" && navigate("/blog"); e.key === "Escape" && navigate("/blog")(e);
document.addEventListener("keydown", handler); document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler); return () => document.removeEventListener("keydown", handler);
@ -84,7 +84,7 @@ const BlogHome: React.FC = () => {
<div <div
data-home data-home
key="blog-home" key="blog-home"
className={classNames( className={cx(
{ "article-open": isArticleOpen, "aside-closed": isAsideClosed }, { "article-open": isArticleOpen, "aside-closed": isAsideClosed },
css` css`
display: flex; display: flex;
@ -132,7 +132,7 @@ const BlogHome: React.FC = () => {
)}> )}>
<button <button
onClick={() => setAsideClosed(closed => !closed)} onClick={() => setAsideClosed(closed => !closed)}
className={classNames( className={cx(
"draw-ctl", "draw-ctl",
css` css`
border: none; border: none;
@ -160,7 +160,7 @@ const BlogHome: React.FC = () => {
)}> )}>
<DrawClose <DrawClose
style={{ width: "2.6rem", height: "1rem" }} style={{ width: "2.6rem", height: "1rem" }}
className={classNames( className={cx(
css` css`
transform: rotate(180deg); transform: rotate(180deg);
transition: transform 100ms; transition: transform 100ms;
@ -182,7 +182,7 @@ const BlogHome: React.FC = () => {
overflow-y: auto; overflow-y: auto;
`}> `}>
<div <div
className={classNames( className={cx(
"blog-list", "blog-list",
css` css`
width: 100%; width: 100%;
@ -206,9 +206,9 @@ const BlogHome: React.FC = () => {
const path = getBlogPath(article); const path = getBlogPath(article);
return ( return (
<Link <span
key={path} key={path}
to={path} onClick={() => navigate(path)}
className={css` className={css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -234,13 +234,13 @@ const BlogHome: React.FC = () => {
<ArticleSubHeader article={article} /> <ArticleSubHeader article={article} />
<Spacer /> <Spacer />
<p>{snippet}</p> <p>{snippet}</p>
</Link> </span>
); );
})} })}
</div> </div>
</aside> </aside>
<article <article
className={classNames( className={cx(
{ "article-open": isArticleOpen }, { "article-open": isArticleOpen },
css` css`
height: 100%; height: 100%;
@ -259,7 +259,7 @@ const BlogHome: React.FC = () => {
)}> )}>
<div <div
key="blog-content" key="blog-content"
className={classNames( className={cx(
css` css`
background: #111111; background: #111111;
position: absolute; position: absolute;

6
src/pages/blog/components/BlogContent.tsx

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom"; import useLocation from "wouter/use-location";
import { marked } from "marked"; import { marked } from "marked";
import { Article, blog, getBlogPath, nextAndPrev } from "../../../data"; import { Article, blog, getBlogPath, nextAndPrev } from "../../../data";
import "../../../blog.css"; import "../../../blog.css";
@ -113,12 +113,12 @@ const btn = css`
export const BlogPost: React.FC = () => { export const BlogPost: React.FC = () => {
const navigate = useNav(); const navigate = useNav();
const location = useLocation(); const [location] = useLocation();
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [year, slug] = location.pathname.split("/").slice(-2); const [year, slug] = location.split("/").slice(-2);
const article = blog[year]?.[slug]; const article = blog[year]?.[slug];
const [next, prev] = nextAndPrev(year, slug); const [next, prev] = nextAndPrev(year, slug);

10
src/pages/main/404.tsx

@ -1,16 +1,18 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom";
import Container from "../../components/Container"; import Container from "../../components/Container";
import { useNav } from "../../util";
function Home() { function Home() {
const navigate = useNav();
return ( return (
<Container> <Container>
<h1>Nothing here</h1> <h1>Nothing here</h1>
<p> <p>
404. Back to{" "} 404. Back to{" "}
<b> <a href="/" onClick={navigate("/")}>
<Link to="/">MKRhere?</Link> MKRhere?
</b> </a>
</p> </p>
</Container> </Container>
); );

39
src/pages/main/Contact.tsx

@ -8,13 +8,17 @@ const A = css`
`; `;
type Contact = { type Contact = {
[k: string]: { value: string; link?: string; replacer?: Record<string, string> }; [k: string]: {
value: string;
link?: string;
replacer?: Record<string, string>;
};
}; };
const CONTACT: Contact = { const CONTACT: Contact = {
Twitter: { value: "MKRhere", link: "https://twitter.com/MKRhere" }, "Twitter/𝕏": { value: "MKRhere", link: "https://twitter.com/MKRhere" },
GitHub: { value: "MKRhere", link: "https://github.com/MKRhere" }, "GitHub": { value: "MKRhere", link: "https://github.com/MKRhere" },
Email: { "Email": {
value: "mυthυkυmαr@thεfεαthεrs.in", value: "mυthυkυmαr@thεfεαthεrs.in",
link: "mailto:mυthυkυmαr@thεfεαthεrs.in", link: "mailto:mυthυkυmαr@thεfεαthεrs.in",
replacer: { replacer: {
@ -23,7 +27,7 @@ const CONTACT: Contact = {
α: "a", α: "a",
}, },
}, },
Phone: { "Phone": {
value: "+9Ι Γ8Δ5 Γ9 8Δ88", value: "+9Ι Γ8Δ5 Γ9 8Δ88",
link: "tel:+91Γ8Δ5Γ98Δ88", link: "tel:+91Γ8Δ5Γ98Δ88",
replacer: { replacer: {
@ -73,8 +77,6 @@ const Home: React.FC = () => {
return ( return (
<Container <Container
next="/"
arrowReversed={true}
className={css` className={css`
min-height: 50vh; min-height: 50vh;
display: flex; display: flex;
@ -85,13 +87,28 @@ const Home: React.FC = () => {
className={css` className={css`
margin-top: auto; margin-top: auto;
display: flex; display: flex;
flex-shrink: 1;
gap: 1rem;
ul { ul {
padding: 0; padding: 0;
margin-left: 1rem; display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 50vw;
li { li {
list-style: none; list-style: none;
min-width: 5rem;
max-width: 100%;
}
li a {
display: block;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} }
} }
`}> `}>
@ -112,7 +129,11 @@ const Home: React.FC = () => {
return ( return (
<li key={key}> <li key={key}>
{value.link ? ( {value.link ? (
<a className={A} href={value.link} target="_blank" rel="noreferrer"> <a
className={A}
href={value.link}
target="_blank"
rel="noreferrer">
{value.value} {value.value}
</a> </a>
) : ( ) : (

103
src/pages/main/Exp.tsx

@ -4,19 +4,32 @@ import Container from "../../components/Container";
const exp = [ const exp = [
{ {
title: "The Feathers",
location: "Chennai (formerly Tirunelveli and Ooty)",
position: "Founder",
year: "2011-19",
},
{
title: "StudioFlicks",
location: "Remote (Coimbatore)",
position: "Co-founder & Creative Head",
year: "2013-15",
},
{
title: "Vinzas", title: "Vinzas",
location: "Chennai", location: "Chennai",
position: "Architectural Intern", position: "Architectural Intern",
year: "2014", year: "2014",
}, },
{ {
title: "BlueCube", title: "Blue Cube",
location: "Chennai", location: "Chennai",
position: "Architectural Intern", position: "Architectural Intern",
year: "2015", year: "2015",
}, },
{ {
title: "OutFocus Magazine", title: "OutFocus Magazine",
location: "Ooty",
position: "Editor / developer", position: "Editor / developer",
year: "2014-17", year: "2014-17",
}, },
@ -40,14 +53,14 @@ const exp = [
}, },
{ {
title: "Hugo's Way", title: "Hugo's Way",
location: "Remote", location: "Remote (Dublin)",
position: "Full stack developer", position: "Full stack developer",
year: "2018-19", year: "2018-19",
}, },
{ {
title: "Navana Tech", title: "Navana Tech",
location: "Remote", location: "Remote (Mumbai)",
position: "Lead web & architect", position: "Lead webdev & architect",
year: "2021-22", year: "2021-22",
}, },
{ {
@ -56,7 +69,7 @@ const exp = [
position: "Chief Maker", position: "Chief Maker",
year: "2019-present", year: "2019-present",
}, },
]; ].reverse();
const Circle: React.FC = () => ( const Circle: React.FC = () => (
<div> <div>
@ -67,7 +80,9 @@ const Circle: React.FC = () => (
background: #333333; background: #333333;
left: -50vw; left: -50vw;
position: absolute; position: absolute;
top: calc(-2rem + 0.25rem / 2 - 0.5px); top: calc(2rem + 0.25rem / 2);
/* centre it to the circle */
transform: translateY(-50%);
z-index: 0; z-index: 0;
`}></div> `}></div>
<div <div
@ -77,7 +92,7 @@ const Circle: React.FC = () => (
background: #ffffff; background: #ffffff;
border-radius: 100%; border-radius: 100%;
position: absolute; position: absolute;
top: -2rem; top: 2rem;
left: 0; left: 0;
z-index: 100; z-index: 100;
`}></div> `}></div>
@ -96,22 +111,36 @@ const ExpUnit: React.FC<Experience> = ({ title, location, position, year }) => {
<div <div
className={css` className={css`
position: relative; position: relative;
display: flex;
flex-direction: column;
gap: 0.6rem;
& * {
line-height: 1em;
}
& h5 {
color: var(--text-subdued);
font-weight: 400;
font-size: 0.9rem;
}
`}> `}>
<Circle /> <Circle />
<h4>{[title, location].filter(Boolean).join(", ")}</h4> <h4>{title}</h4>
<span <div>
className={css` <span
color: #bdbdbd; className={css`
`}> color: var(--text-colour);
{position} `}>
</span> {position}
{" . "} </span>
<span {" . "}
className={css` <span
font-weight: 300; className={css`
`}> font-weight: 300;
{year} `}>
</span> {year}
</span>
</div>
<h5>{location}</h5>
</div> </div>
); );
}; };
@ -130,21 +159,33 @@ const age = getAge("27 May 1995");
const Exp: React.FC = () => { const Exp: React.FC = () => {
return ( return (
<Container next="/projects"> <Container>
<h2>Im a {age} year old developer from Chennai, India.</h2> <h2>
<p>Here are some places Ive worked at in reverse chronological order:</p> Im a {age} year old developer from
<br />
Chennai, India.
</h2>
<p>
Here are some places Ive worked at{" "}
<span
className={css`
/* font-size: 0.8rem; */
color: var(--text-subdued);
`}>
(recent first)
</span>
:
</p>
<div <div
className={css` className={css`
display: flex;
flex-direction: row-reverse;
width: 100%; width: 100%;
flex-wrap: wrap-reverse;
display: grid;
grid-template-columns: repeat(auto-fit, 20rem);
gap: 1rem;
& > * { & > * {
flex-basis: 15rem; padding-top: 4rem;
flex-grow: 1;
margin-top: 4rem;
margin-right: 3%;
} }
`}> `}>
{exp.map(unit => ( {exp.map(unit => (

2
src/pages/main/Home.tsx

@ -4,7 +4,7 @@ import Dashed from "../../components/Dashed";
const Home: React.FC = () => { const Home: React.FC = () => {
return ( return (
<Container next="/experience"> <Container>
<h1>MKRhere</h1> <h1>MKRhere</h1>
<p> <p>
Web home of <Dashed>designer</Dashed>, <Dashed>developer</Dashed>, and{" "} Web home of <Dashed>designer</Dashed>, <Dashed>developer</Dashed>, and{" "}

2
src/pages/main/Projects.tsx

@ -146,7 +146,7 @@ const ProjectUnit: React.FC<Project> = ({
const Exp: React.FC = () => { const Exp: React.FC = () => {
return ( return (
<Container next="/contact"> <Container>
<h2>What else have I built?</h2> <h2>What else have I built?</h2>
<p>Some tools, libraries, and apps over time:</p> <p>Some tools, libraries, and apps over time:</p>
<div <div

27
src/util/index.ts

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import useLocation from "wouter/use-location";
export const getTimeout = () => { export const getTimeout = () => {
const clearables = new Set<number>(); const clearables = new Set<number>();
@ -21,9 +21,10 @@ export const ellipses = (text: string, length: number = 100) =>
text.length > length ? text.slice(0, length - 3) + "..." : text; text.length > length ? text.slice(0, length - 3) + "..." : text;
export const useNav = () => { export const useNav = () => {
const navigate = useNavigate(); const [, navigate] = useLocation();
return (link: string) => (e: React.MouseEvent) => { return (link: string) => (e: React.MouseEvent | KeyboardEvent) => {
e?.preventDefault();
if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener"); if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener");
navigate(link); navigate(link);
}; };
@ -34,3 +35,23 @@ export function rewriteExtn(filename: string, extn: string) {
split[split.length - 1] = extn; split[split.length - 1] = extn;
return split.join("."); return split.join(".");
} }
export function normalise(path: string) {
return (
(path.startsWith("/") ? "/" : "") +
path.trim().split("/").filter(Boolean).join("/")
);
}
export function comparePaths(p1: string, p2: string) {
return normalise(p1) === normalise(p2);
}
export const get = {
next<X>(xs: X[], i: number) {
return xs.at((i + 1) % xs.length)!;
},
prev<X>(xs: X[], i: number) {
return xs.at((i - 1) % xs.length)!;
},
};

9
vite.config.ts

@ -5,8 +5,13 @@ import svgr from "@svgr/rollup";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
server: { port: 3000 }, server: { port: 10000 },
plugins: [react(), Object.assign(svgr({ ref: true, svgo: false }), { enforce: "pre" } as const)], plugins: [
react(),
Object.assign(svgr({ ref: true, svgo: false }), {
enforce: "pre",
} as const),
],
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {

Loading…
Cancel
Save